From 18c116b40750d41dc523266e2f3c19a3572269d2 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Fri, 8 Dec 2023 13:33:54 +0900 Subject: [PATCH 01/82] =?UTF-8?q?Style:=20=EC=BD=94=EB=93=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/purchase/PurchaseService.java | 12 +++--------- .../domain/purchase/dto/PurchaseRequestDto.java | 5 ++--- .../domain/recipe/BackOfficeRecipeController.java | 6 +++--- .../purebasketbe/domain/recipe/RecipeService.java | 12 ++---------- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 114030b..4986eea 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -32,21 +32,20 @@ public class PurchaseService { private final int PRODUCTS_PER_PAGE = 20; - @Transactional public void purchaseProducts(final List purchaseRequestDto, Member member) { int size = purchaseRequestDto.size(); List sortedPurchaseDetailList = purchaseRequestDto.stream() .sorted(Comparator.comparing(PurchaseDetail::getProductId)).toList(); - List requestIds = sortedPurchaseDetailList.stream().map(PurchaseDetail::getProductId).toList(); + List requestIds = sortedPurchaseDetailList.stream() + .map(PurchaseDetail::getProductId).toList(); + List validProductList = productRepository.findByIdIn(requestIds); validateProducts(size, validProductList); - List amountList = sortedPurchaseDetailList.stream() .map(PurchaseDetail::getAmount).toList(); List purchaseList = new ArrayList<>(); - for (int i = 0; i < size; i++) { Product product = validProductList.get(i); int amount = amountList.get(i); @@ -63,13 +62,8 @@ public void purchaseProducts(final List purchaseRequestDto, Memb cartRepository.deleteByUserAndProductIn(member, validProductList); } - - - @Transactional(readOnly = true) - public Page getPurchases(Member member, int page, String sortBy, String order) { - Sort.Direction direction = Direction.valueOf(order.toUpperCase()); Sort sort = Sort.by(direction, sortBy); Pageable pageable = PageRequest.of(page, PRODUCTS_PER_PAGE, sort); diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java index 4c12346..f540822 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java @@ -2,12 +2,12 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.validation.annotation.Validated; + +import java.util.List; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -26,5 +26,4 @@ public static class PurchaseDetail { @Min(value = 1, message = "1개 이상 입력하세요") private int amount; } - } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java index d0c5133..6aa022f 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java @@ -17,18 +17,18 @@ @RequiredArgsConstructor @RequestMapping("/api/backoffice/recipes") public class BackOfficeRecipeController { + private final RecipeService recipeService; @GetMapping public ResponseEntity> getRecipes(@RequestParam(defaultValue = "1") int page) { Page responseBody = recipeService.getRecipes(page - 1); - return ResponseEntity.status(HttpStatus.OK) - .body(responseBody); + return ResponseEntity.status(HttpStatus.OK).body(responseBody); } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity registerRecipe(@RequestPart("requestDto") @Validated RecipeRequestDto requestDto, - @RequestPart("file") MultipartFile file) { + @RequestPart("file") MultipartFile file) { recipeService.registerRecipe(requestDto, file); return ResponseEntity.status(HttpStatus.CREATED) .location(URI.create("/api/backoffice/recipes")) diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java index 7ff64d8..e041075 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java @@ -6,7 +6,6 @@ import com.example.purebasketbe.domain.recipe.dto.RecipeResponseDto; import com.example.purebasketbe.domain.recipe.entity.Recipe; import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; -import com.example.purebasketbe.domain.recipe.entity.RecipeProductRepository; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import io.awspring.cloud.s3.ObjectMetadata; @@ -28,7 +27,9 @@ @Service @RequiredArgsConstructor public class RecipeService { + private final RecipeRepository recipeRepository; + private final ProductRepository productRepository; private final S3Template s3Template; @Value("${aws.bucket.name}") @@ -36,8 +37,6 @@ public class RecipeService { @Value("${spring.cloud.aws.region.static}") private String region; private final int RECIPES_PER_PAGE = 10; - private final ProductRepository productRepository; - private final RecipeProductRepository recipeProductRepository; @Transactional(readOnly = true) public Page getRecipes(int page) { @@ -72,7 +71,6 @@ public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { } recipeRepository.save(recipe); - // S3 저장은 commit이 된 이후에 수행되어야 한다. uploadImage(file, key); } @@ -86,12 +84,8 @@ public void deleteProduct(Long recipeId) { // ToDo: S3에서 사진 삭제하기 코드 추가 } - - - // ToDo: productService에 있는 메서드와 합친 후에 S3 관련 폴더 생성하기 private void uploadImage(MultipartFile file, String key) { - InputStream inputStream; try { inputStream = file.getInputStream(); @@ -100,9 +94,7 @@ private void uploadImage(MultipartFile file, String key) { } ObjectMetadata metadata = ObjectMetadata.builder().contentType("text/plain").build(); - s3Template.upload(bucket, key, inputStream, metadata); - } private Product findValidProduct(Long productId) { From 53196b82a59a5682ab5ed1db0c12a24e51e79aaa Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Fri, 8 Dec 2023 15:32:56 +0900 Subject: [PATCH 02/82] =?UTF-8?q?Feat:=20RedisConfig,=20RedisService=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RedisConfig.java | 35 ++++++++++ .../global/redis/RedisService.java | 67 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisConfig.java create mode 100644 src/main/java/com/example/purebasketbe/global/redis/RedisService.java diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java new file mode 100644 index 0000000..e53b288 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.example.purebasketbe.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@RequiredArgsConstructor +@Configuration +@EnableRedisRepositories +public class RedisConfig { + private final RedisProperties redisProperties; + + // RedisProperties로 yaml에 저장한 host, post를 연결 + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + return redisTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/redis/RedisService.java b/src/main/java/com/example/purebasketbe/global/redis/RedisService.java new file mode 100644 index 0000000..e0a360e --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/redis/RedisService.java @@ -0,0 +1,67 @@ +package com.example.purebasketbe.global.redis; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + + public void setValues(String key, String data) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data); + } + + public void setValues(String key, String data, Duration duration) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data, duration); + } + + @Transactional(readOnly = true) + public String getValues(String key) { + ValueOperations values = redisTemplate.opsForValue(); + if (values.get(key) == null) { + return "false"; + } + return (String) values.get(key); + } + + public void deleteValues(String key) { + redisTemplate.delete(key); + } + + public void expireValues(String key, int timeout) { + redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS); + } + + public void setHashOps(String key, Map data) { + HashOperations values = redisTemplate.opsForHash(); + values.putAll(key, data); + } + + @Transactional(readOnly = true) + public String getHashOps(String key, String hashKey) { + HashOperations values = redisTemplate.opsForHash(); + return Boolean.TRUE.equals(values.hasKey(key, hashKey)) ? (String) redisTemplate.opsForHash().get(key, hashKey) : ""; + } + + public void deleteHashOps(String key, String hashKey) { + HashOperations values = redisTemplate.opsForHash(); + values.delete(key, hashKey); + } + + public boolean checkExistsValue(String value) { + return !value.equals("false"); + } +} \ No newline at end of file From 05555dff427f9e5c3b7ecaf85c6351d751dae5af Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Fri, 8 Dec 2023 15:33:27 +0900 Subject: [PATCH 03/82] =?UTF-8?q?Feat:=20RefreshToken=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +- settings.gradle | 2 +- .../domain/member/MemberRepository.java | 1 - .../domain/member/MemberService.java | 4 + .../global/config/WebSecurityConfig.java | 6 +- .../global/exception/ErrorCode.java | 5 +- .../filter/JwtAuthenticationFilter.java | 19 ++++- .../global/security/jwt/JwtUtil.java | 74 +++++++++++++++---- 8 files changed, 93 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index d47b429..8400a6a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,10 +24,13 @@ repositories { } dependencies { + //redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // aws implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.0.0' implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0' - +s // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' diff --git a/settings.gradle b/settings.gradle index 7b90dea..04b562b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'pure-basket-be' +rootProject.name = 'Pure-Basket-BE' diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java index 576fcb8..f2a35b9 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java @@ -12,5 +12,4 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); - } diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java index e762b7a..d79cd0e 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java @@ -31,4 +31,8 @@ private void checkIfEmailExist(String email) { } } + public Member findMemberByEmail(String email) { + return memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.EMAIL_NOT_FOUND)); + } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java index 00936c8..bb84c00 100644 --- a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java @@ -1,5 +1,7 @@ package com.example.purebasketbe.global.config; +import com.example.purebasketbe.domain.member.MemberService; +import com.example.purebasketbe.global.redis.RedisService; import com.example.purebasketbe.global.security.filter.JwtAuthenticationFilter; import com.example.purebasketbe.global.security.filter.JwtAuthorizationFilter; import com.example.purebasketbe.global.security.impl.UserDetailsServiceImpl; @@ -40,6 +42,8 @@ public class WebSecurityConfig { private final JwtUtil jwtUtil; private final UserDetailsServiceImpl userDetailsService; private final AuthenticationConfiguration authenticationConfiguration; + private final RedisService redisService; + private final MemberService memberService; @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { @@ -48,7 +52,7 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { - JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil); + JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil, redisService, memberService); filter.setAuthenticationManager(authenticationManager(authenticationConfiguration)); return filter; } diff --git a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java index 5acfacb..f8c3b0d 100644 --- a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java +++ b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java @@ -9,6 +9,7 @@ public enum ErrorCode { EMAIL_DIFFERENT_FORMAT(HttpStatus.BAD_REQUEST.value(), "이메일 형식이 올바르지 않습니다."), + EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "이메일을 찾을 수 없습니다"), PASSWORD_DIFFERENT_FORMAT(HttpStatus.BAD_REQUEST.value(), "비밀번호는 8~15자리로, 알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다."), PRODUCT_ALREADY_EXISTS(HttpStatus.CONTINUE.value(), "이미 등록된 물건입니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 등록된 이메일입니다."), @@ -35,13 +36,15 @@ public enum ErrorCode { PHONENUMBER_ALREADY_EXISTS(HttpStatus.CONTINUE.value(), "이미 등록된 전화번호입니다."), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 만료되었습니다."), EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 유효하지 않습니다."), + TOKEN_INVALID(HttpStatus.UNAUTHORIZED.value(), "토큰이 유효하지 않습니다."), INVALID_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 유효하지 않습니다."), INVALID_QUANTITY(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 수량입니다."), NOT_FOUND_TOKEN(HttpStatus.UNAUTHORIZED.value(), "엑세스 토큰을 찾을 수 없습니다."), NOT_FOUND_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED.value(), "로그인 후 이용해 주세요."), OUT_OF_RANGE(HttpStatus.BAD_REQUEST.value(), "요청한 페이지 범위가 적절하지 않습니다."), TOKEN_NOT_EXIST(HttpStatus.UNAUTHORIZED.value(), "이 기능을 사용하기 위해서는 로그인이 필요합니다."), + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "만료된 토큰입니다."), + TOKEN_UNSUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), "지원하지 않는 토큰형식입니다"), ELEMENTS_IS_REQUIRED(HttpStatus.BAD_REQUEST.value(), "필수 입력 필드가 누락되었습니다."), ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST.value(), "포인트가 부족합니다."), diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 2422db3..31ff098 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -1,8 +1,10 @@ package com.example.purebasketbe.global.security.filter; +import com.example.purebasketbe.domain.member.MemberService; import com.example.purebasketbe.domain.member.dto.LoginRequestDto; import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.member.entity.UserRole; +import com.example.purebasketbe.global.redis.RedisService; import com.example.purebasketbe.global.security.impl.UserDetailsImpl; import com.example.purebasketbe.global.security.jwt.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; @@ -17,15 +19,20 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.io.IOException; +import java.time.Duration; @Slf4j(topic = "로그인 및 JWT 생성") public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private final JwtUtil jwtUtil; + private final RedisService redisService; + private final MemberService memberService; private final ObjectMapper objectMapper = new ObjectMapper(); - public JwtAuthenticationFilter(JwtUtil jwtUtil) { + public JwtAuthenticationFilter(JwtUtil jwtUtil, RedisService redisService, MemberService memberService) { this.jwtUtil = jwtUtil; + this.redisService = redisService; + this.memberService = memberService; setFilterProcessesUrl("/api/auth/login"); } @@ -37,7 +44,6 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ try { LoginRequestDto requestDto = objectMapper.readValue(request.getInputStream(), LoginRequestDto.class); - log.info("Email: {}", requestDto.email()); return getAuthenticationManager().authenticate( @@ -65,7 +71,16 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR UserRole role = ((UserDetailsImpl) authResult.getPrincipal()).getMember().getRole(); String token = jwtUtil.createToken(email, role); + String refreshToken = jwtUtil.createRefreshToken(email, role); jwtUtil.addJwtToHeader(JwtUtil.AUTHORIZATION_HEADER, token, response); + jwtUtil.addJwtToHeader(JwtUtil.REFRESHTOKEN_HEADER,refreshToken,response); + + // 로그인 성공시 Refresh Token Redis 저장 ( key = Email / value = Refresh Token ) + UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); + Member findMember = memberService.findMemberByEmail(userDetails.getUsername()); + long refreshTokenExpirationMillis = jwtUtil.getRefreshTokenExpirationMillis(); + redisService.setValues(findMember.getEmail(), refreshToken, Duration.ofMillis(refreshTokenExpirationMillis)); + // 성공 메시지와 토큰을 JSON 형식으로 응답에 추가 response.setContentType("application/json;charset=UTF-8"); diff --git a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java index df394b6..7d64700 100644 --- a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java +++ b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java @@ -1,12 +1,16 @@ package com.example.purebasketbe.global.security.jwt; import com.example.purebasketbe.domain.member.entity.UserRole; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -14,36 +18,55 @@ import org.springframework.util.StringUtils; import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.Key; -import java.util.Base64; import java.util.Date; +@Slf4j @Component public class JwtUtil { // Header KEY 값 public static final String AUTHORIZATION_HEADER = "Authorization"; + // Refreshtoken Header KEY 값 + public static final String REFRESHTOKEN_HEADER = "Refreshtoken"; // 사용자 권한 값의 KEY public static final String AUTHORIZATION_KEY = "auth"; // Token 식별자 public static final String BEARER_PREFIX = "Bearer "; // 토큰 만료시간 - private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분 @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey private String secretKey; + + @Getter + @Value("${jwt.access-token-expiration-millis}") + private long accessTokenExpirationMillis; + + @Getter + @Value("${jwt.refresh-token-expiration-millis}") + private long refreshTokenExpirationMillis; + private Key key; private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 로그 설정 public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그"); + // Bean 등록후 Key SecretKey HS256 decode @PostConstruct public void init() { - byte[] bytes = Base64.getDecoder().decode(secretKey); - key = Keys.hmacShaKeyFor(bytes); + String base64EncodedSecretKey = encodeBase64SecretKey(this.secretKey); + this.key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); + } + + public String encodeBase64SecretKey(String secretKey) { + return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { + byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); + return Keys.hmacShaKeyFor(keyBytes); } // 토큰 생성 @@ -54,12 +77,25 @@ public String createToken(String username, UserRole role) { Jwts.builder() .setSubject(username) // 사용자 식별자값(ID) .claim(AUTHORIZATION_KEY, role) // 사용자 권한 - .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간 + .setExpiration(new Date(date.getTime() + accessTokenExpirationMillis)) // 만료 시간 .setIssuedAt(date) // 발급일 .signWith(key, signatureAlgorithm) // 암호화 알고리즘 .compact(); //압축 후 생성 } + // 리프세쉬 토큰 생성 + public String createRefreshToken(String username, UserRole role) { + Date date = new Date(); + + return BEARER_PREFIX + + Jwts.builder() + .setSubject(username) + .setExpiration(new Date(date.getTime() + refreshTokenExpirationMillis)) + .setIssuedAt(date) + .signWith(key, signatureAlgorithm) + .compact(); + } + // JWT Header 에 저장 public void addJwtToHeader(String header, String token, HttpServletResponse res) { try { @@ -83,18 +119,24 @@ public String substringToken(String tokenValue) { // 토큰 검증 public boolean validateToken(String token) { try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); - return true; - } catch (SecurityException | MalformedJwtException | SignatureException e) { - logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);; + } catch (MalformedJwtException e) { + log.info("Invalid JWT token, 유효하지 않는 JWT 서명 입니다."); + log.trace("Invalid JWT token trace = {e}", e); } catch (ExpiredJwtException e) { - logger.error("Expired JWT token, 만료된 JWT token 입니다."); + log.info("Expired JWT token, 만료된 JWT token 입니다."); + log.trace("Expired JWT token trace = {e}", e); + throw new CustomException(ErrorCode.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { - logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); + log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); + log.trace("Unsupported JWT token trace = {e}", e); + throw new CustomException(ErrorCode.TOKEN_UNSUPPORTED); } catch (IllegalArgumentException e) { - logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); + log.info("JWT claims string is empty, 잘못된 JWT 토큰 입니다."); + log.trace("JWT claims string is empty trace = {e}", e); + throw new CustomException(ErrorCode.TOKEN_INVALID); } - return false; + return true; } // 토큰에서 사용자 정보 가져오기 From 22d67aa78037265021fe3d325af59dc918d37f6f Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Fri, 8 Dec 2023 15:52:27 +0900 Subject: [PATCH 04/82] =?UTF-8?q?Update:=20MEMEBER=20=EC=A0=91=EA=B7=BC?= =?UTF-8?q?=EB=B6=88=EA=B0=80=20PreAuthorize=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/BackOfficeProductController.java | 2 ++ .../purebasketbe/domain/recipe/BackOfficeRecipeController.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java b/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java index 3de05fa..8ff4fe2 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java @@ -6,6 +6,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -15,6 +16,7 @@ @RestController @RequiredArgsConstructor +@PreAuthorize("!hasAuthority('ROLE_MEMBER')") @RequestMapping("/api/backoffice/products") public class BackOfficeProductController { diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java index d0c5133..04eab32 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java @@ -7,6 +7,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -15,6 +16,7 @@ @RestController @RequiredArgsConstructor +@PreAuthorize("!hasAuthority('ROLE_MEMBER')") @RequestMapping("/api/backoffice/recipes") public class BackOfficeRecipeController { private final RecipeService recipeService; From ee5b1c6a4f24b1de0bea5d338729c3e071976d2c Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Fri, 8 Dec 2023 16:48:22 +0900 Subject: [PATCH 05/82] =?UTF-8?q?Refactor:=20=EC=8B=9C=ED=81=90=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=EB=B0=8F=20JWT=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/WebSecurityConfig.java | 9 ++----- .../filter/JwtAuthenticationFilter.java | 1 - .../global/security/jwt/JwtUtil.java | 26 +++++++++---------- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java index bb84c00..8fbd3c2 100644 --- a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java @@ -26,8 +26,8 @@ import java.util.List; @Configuration -@RequiredArgsConstructor @EnableWebSecurity +@RequiredArgsConstructor @EnableMethodSecurity(securedEnabled = true) public class WebSecurityConfig { @@ -77,15 +77,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // 접근 권한 설정 http.authorizeHttpRequests((authorizeHttpRequests) -> - -// authorizeHttpRequests -// .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() -// .requestMatchers(AUTH_WHITELIST).permitAll() -// .requestMatchers(HttpMethod.GET, "/api/stores/**").permitAll() -// .anyRequest().authenticated()); authorizeHttpRequests .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .requestMatchers(AUTH_WHITELIST).permitAll() + .requestMatchers("/api/backoffice/**").hasRole(("ADMIN")) .anyRequest().permitAll()); diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 31ff098..503ac27 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -64,7 +64,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR FilterChain chain, Authentication authResult) throws IOException { - log.info("로그인 성공 및 JWT 생성"); String email = ((UserDetailsImpl) authResult.getPrincipal()).getUsername(); diff --git a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java index 7d64700..0351871 100644 --- a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java +++ b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java @@ -34,8 +34,8 @@ public class JwtUtil { public static final String AUTHORIZATION_KEY = "auth"; // Token 식별자 public static final String BEARER_PREFIX = "Bearer "; - // 토큰 만료시간 + // 토큰 만료시간 @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey private String secretKey; @@ -74,16 +74,16 @@ public String createToken(String username, UserRole role) { Date date = new Date(); return BEARER_PREFIX + - Jwts.builder() - .setSubject(username) // 사용자 식별자값(ID) - .claim(AUTHORIZATION_KEY, role) // 사용자 권한 - .setExpiration(new Date(date.getTime() + accessTokenExpirationMillis)) // 만료 시간 - .setIssuedAt(date) // 발급일 - .signWith(key, signatureAlgorithm) // 암호화 알고리즘 - .compact(); //압축 후 생성 + Jwts.builder() + .setSubject(username) // 사용자 식별자값(ID) + .claim(AUTHORIZATION_KEY, role) // 사용자 권한 + .setExpiration(new Date(date.getTime() + accessTokenExpirationMillis)) // 만료 시간 + .setIssuedAt(date) // 발급일 + .signWith(key, signatureAlgorithm) // 암호화 알고리즘 + .compact(); //압축 후 생성 } - // 리프세쉬 토큰 생성 + // 리프레쉬 토큰 생성 public String createRefreshToken(String username, UserRole role) { Date date = new Date(); @@ -100,9 +100,9 @@ public String createRefreshToken(String username, UserRole role) { public void addJwtToHeader(String header, String token, HttpServletResponse res) { try { token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); - res.addHeader(header,token); + res.addHeader(header, token); } catch (UnsupportedEncodingException e) { - logger.error(e.getMessage()+"헤더로 토큰 전달"); + log.info(e.getMessage() + "헤더로 토큰 전달"); } } @@ -112,14 +112,14 @@ public String substringToken(String tokenValue) { if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) { return tokenValue.substring(7); } - logger.error("Not Found Token"); + log.info("Not Found Token"); throw new NullPointerException("Not Found Token"); } // 토큰 검증 public boolean validateToken(String token) { try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);; + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); } catch (MalformedJwtException e) { log.info("Invalid JWT token, 유효하지 않는 JWT 서명 입니다."); log.trace("Invalid JWT token trace = {e}", e); From 56e7eb74bd0334bed33fbe48553b2ee40bbecdc5 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Fri, 8 Dec 2023 17:06:29 +0900 Subject: [PATCH 06/82] =?UTF-8?q?Update:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EB=A7=8C=EB=A3=8C=EC=8B=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=B8=A1?= =?UTF-8?q?=EC=97=90=20SC=5FUNAUTHORIZED=20=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/filter/JwtAuthenticationFilter.java | 9 +++++++++ .../purebasketbe/global/security/jwt/JwtUtil.java | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 503ac27..2ce8673 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -69,11 +69,20 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String email = ((UserDetailsImpl) authResult.getPrincipal()).getUsername(); UserRole role = ((UserDetailsImpl) authResult.getPrincipal()).getMember().getRole(); + + String token = jwtUtil.createToken(email, role); String refreshToken = jwtUtil.createRefreshToken(email, role); jwtUtil.addJwtToHeader(JwtUtil.AUTHORIZATION_HEADER, token, response); jwtUtil.addJwtToHeader(JwtUtil.REFRESHTOKEN_HEADER,refreshToken,response); + // 리프레시 토큰 만료 여부 확인 + if (jwtUtil.isRefreshTokenExpired(refreshToken)) { + response.setContentType("application/json;charsetUTF=8"); + response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("재로그인 필요"))); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + // 로그인 성공시 Refresh Token Redis 저장 ( key = Email / value = Refresh Token ) UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); Member findMember = memberService.findMemberByEmail(userDetails.getUsername()); diff --git a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java index 0351871..1c8c7a0 100644 --- a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java +++ b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java @@ -139,6 +139,15 @@ public boolean validateToken(String token) { return true; } + public boolean isRefreshTokenExpired(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return false; + } catch (ExpiredJwtException e) { + return true; + } + } + // 토큰에서 사용자 정보 가져오기 public Claims getUserInfoFromToken(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); From 52d7db63b050302a07235433b4e7de4575780c3e Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 8 Dec 2023 18:31:19 +0900 Subject: [PATCH 07/82] =?UTF-8?q?Refactor:=20S3=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S3Handler class 생성 및 코드 리팩토링 --- build.gradle | 2 +- .../domain/cart/CartController.java | 9 ++- .../purebasketbe/domain/cart/CartService.java | 2 +- .../product/BackOfficeProductController.java | 9 +-- .../domain/product/ProductRepository.java | 2 +- .../domain/product/ProductService.java | 61 +++++------------ .../domain/product/entity/Product.java | 4 +- .../domain/purchase/PurchaseController.java | 5 +- .../recipe/BackOfficeRecipeController.java | 10 +-- .../{entity => }/RecipeProductRepository.java | 3 +- .../domain/recipe/RecipeService.java | 36 ++-------- .../purebasketbe/global/s3/S3Handler.java | 67 +++++++++++++++++++ 12 files changed, 119 insertions(+), 91 deletions(-) rename src/main/java/com/example/purebasketbe/domain/recipe/{entity => }/RecipeProductRepository.java (64%) create mode 100644 src/main/java/com/example/purebasketbe/global/s3/S3Handler.java diff --git a/build.gradle b/build.gradle index 8400a6a..df67569 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ dependencies { // aws implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.0.0' implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0' -s + // swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartController.java b/src/main/java/com/example/purebasketbe/domain/cart/CartController.java index 81daf5f..2893eb9 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartController.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartController.java @@ -7,6 +7,7 @@ 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 java.net.URI; @@ -20,7 +21,7 @@ public class CartController { private final CartService cartService; @PostMapping("/{productId}") - public ResponseEntity addToCart(@PathVariable Long productId, @RequestBody CartRequestDto requestDto, + public ResponseEntity addToCart(@PathVariable Long productId, @RequestBody @Validated CartRequestDto requestDto, @LoginAccount Member member) { cartService.addToCart(productId, requestDto, member); return ResponseEntity.status(HttpStatus.OK).build(); @@ -33,7 +34,7 @@ public ResponseEntity> getCartList(@LoginAccount Member me } @PutMapping("/{productId}") - public ResponseEntity updateCart(@PathVariable Long productId, @RequestBody CartRequestDto requestDto, + public ResponseEntity updateCart(@PathVariable Long productId, @RequestBody @Validated CartRequestDto requestDto, @LoginAccount Member member) { cartService.updateCart(productId, requestDto, member); return ResponseEntity.status(HttpStatus.OK).build(); @@ -49,6 +50,8 @@ public ResponseEntity deleteCart(@PathVariable Long productId, @LoginAccou public ResponseEntity addRecipeRelatedProductsToCart(@PathVariable Long recipeId, @LoginAccount Member member) { cartService.addRecipeRelatedProductsToCarts(recipeId, member); - return ResponseEntity.status(HttpStatus.CREATED).location(URI.create("/api/carts")).build(); + return ResponseEntity.status(HttpStatus.CREATED) + .location(URI.create("/api/carts")) + .build(); } } diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index 9125036..0b21073 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -73,7 +73,7 @@ private Product findAndValidateProduct(Long productId) { Product product = productRepository.findById(productId).orElseThrow( () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) ); - if (product.getStock() == 0 || product.isDeleted()) { + if (product.getStock() <= 0 || product.isDeleted()) { throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); } return product; diff --git a/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java b/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java index 8ff4fe2..618f467 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/BackOfficeProductController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.net.URI; import java.util.List; import java.util.Optional; @@ -21,7 +22,7 @@ public class BackOfficeProductController { private final ProductService productService; - private final String adminLandingPath = "/api/backoffice/products"; + private final String ADMIN_LANDING_PATH = "/api/backoffice/products"; @GetMapping public ResponseEntity getProducts( @@ -46,7 +47,7 @@ public ResponseEntity registerProduct(@RequestPart(value = "dto") @Validat @RequestPart(value = "files") List files) { productService.registerProduct(requestDto, files); return ResponseEntity.status(HttpStatus.SEE_OTHER) - .header("Location", adminLandingPath) + .location(URI.create(ADMIN_LANDING_PATH)) .build(); } @@ -57,7 +58,7 @@ public ResponseEntity updateProduct(@PathVariable Long productId, List fileList = Optional.ofNullable(files).orElse(List.of()); productService.updateProduct(productId, requestDto, fileList); return ResponseEntity.status(HttpStatus.SEE_OTHER) - .header("Location", adminLandingPath) + .location(URI.create(ADMIN_LANDING_PATH)) .build(); } @@ -65,7 +66,7 @@ public ResponseEntity updateProduct(@PathVariable Long productId, public ResponseEntity deleteProduct(@PathVariable Long productId) { productService.deleteProduct(productId); return ResponseEntity.status(HttpStatus.SEE_OTHER) - .header("Location", adminLandingPath) + .location(URI.create(ADMIN_LANDING_PATH)) .build(); } } diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index 7c76312..93046ac 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -24,5 +24,5 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet List findByIdIn(List requestIds); - Optional findByIdAndDeleted(Long productId, boolean deleted); + Optional findByIdAndDeleted(Long productId, boolean isDeleted); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 53628f4..95b3500 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -6,8 +6,7 @@ import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; -import io.awspring.cloud.s3.ObjectMetadata; -import io.awspring.cloud.s3.S3Template; +import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.*; @@ -15,11 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.util.*; @Service @RequiredArgsConstructor @@ -27,17 +22,12 @@ public class ProductService { private final ProductRepository productRepository; private final ImageRepository imageRepository; - private final S3Template s3Template; + private final S3Handler s3Handler; @Value("${products.event.page.size}") private int eventPageSize; @Value("${products.page.size}") private int pageSize; - @Value("${aws.bucket.name}") - private String bucket; - @Value("${spring.cloud.aws.region.static}") - private String region; - @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { @@ -91,11 +81,9 @@ public ProductDetailResponseDto getProduct(Long productId) { public void registerProduct(ProductRequestDto requestDto, List files) { checkExistProductByName(requestDto.name()); Product newProduct = Product.from(requestDto); - productRepository.save(newProduct); - for (MultipartFile file : files) { - saveAndUploadImage(file, newProduct); - } + productRepository.save(newProduct); + saveAndUploadImage(newProduct, files); } @Transactional @@ -104,9 +92,7 @@ public void updateProduct(Long productId, ProductRequestDto requestDto, List s3Template.deleteObject(bucket, getKey(image.getImgUrl()))); + .forEach(image -> s3Handler.deleteImage(image.getImgUrl())); imageRepository.deleteAllByProductId(productId); } @@ -140,28 +126,17 @@ private List getImgUrlList(Product product) { .toList(); } - private void saveAndUploadImage(MultipartFile file, Product product) { - String key = UUID.randomUUID() + "_" + file.getOriginalFilename(); - InputStream inputStream; - try { - inputStream = file.getInputStream(); - } catch (IOException e) { - throw new CustomException(ErrorCode.INVALID_IMAGE); + private void saveAndUploadImage(Product product, List files) { + List imageList = new ArrayList<>(); + List imgUrlList = new ArrayList<>(); + for (MultipartFile file : files) { + String imgUrl = s3Handler.makeUrl(file); + Image image = Image.of(imgUrl, product); + imageList.add(image); + imgUrlList.add(imgUrl); } - ObjectMetadata metadata = ObjectMetadata.builder().contentType("text/plain").build(); - - String publicUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key); - Image newimage = Image.of(publicUrl, product); - imageRepository.save(newimage); - - s3Template.upload(bucket, key, inputStream, metadata); - } - - private String getKey(String imgUrl) { - int lastIndex = imgUrl.lastIndexOf("/"); - if (lastIndex != -1 && lastIndex < imgUrl.length() - 1) { - return imgUrl.substring(lastIndex + 1); - } else throw new CustomException(ErrorCode.INVALID_IMAGE); + imageRepository.saveAll(imageList); + s3Handler.uploadImages(imgUrlList, files); } private void checkExistProductByName(String name) { @@ -171,7 +146,7 @@ private void checkExistProductByName(String name) { } private Product findProduct(Long id) { - return productRepository.findById(id).orElseThrow( + return productRepository.findByIdAndDeleted(id, false).orElseThrow( () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) ); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index b09ad94..0d63cf5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -12,6 +12,8 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; + @Entity @Getter @@ -95,7 +97,7 @@ public void update(ProductRequestDto requestDto) { } public void softDelete() { - this.name += "-deleted"; + this.name += "-deleted-" + UUID.randomUUID(); this.modifiedAt = LocalDateTime.now(); this.deleted = true; } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index 2f5a6a1..be32f5a 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -9,6 +9,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @@ -19,9 +20,9 @@ public class PurchaseController { private final PurchaseService purchaseService; @PostMapping - public ResponseEntity purchaseProducts(@RequestBody PurchaseRequestDto purchaseRequestDto, + public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseRequestDto requestDto, @LoginAccount Member member) { - purchaseService.purchaseProducts(purchaseRequestDto.getPurchaseList(), member); + purchaseService.purchaseProducts(requestDto.getPurchaseList(), member); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java index 1845b1c..4c6afb7 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/BackOfficeRecipeController.java @@ -22,6 +22,8 @@ public class BackOfficeRecipeController { private final RecipeService recipeService; + private final String ADMIN_RECIPE_PATH = "/api/backoffice/recipes"; + @GetMapping public ResponseEntity> getRecipes(@RequestParam(defaultValue = "1") int page) { Page responseBody = recipeService.getRecipes(page - 1); @@ -29,19 +31,19 @@ public ResponseEntity> getRecipes(@RequestParam(defaultV } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity registerRecipe(@RequestPart("requestDto") @Validated RecipeRequestDto requestDto, + public ResponseEntity registerRecipe(@RequestPart("dto") @Validated RecipeRequestDto requestDto, @RequestPart("file") MultipartFile file) { recipeService.registerRecipe(requestDto, file); return ResponseEntity.status(HttpStatus.CREATED) - .location(URI.create("/api/backoffice/recipes")) + .location(URI.create(ADMIN_RECIPE_PATH)) .build(); } @DeleteMapping("/{recipeId}") public ResponseEntity deleteRecipe(@PathVariable Long recipeId) { - recipeService.deleteProduct(recipeId); + recipeService.deleteRecipe(recipeId); return ResponseEntity.status(HttpStatus.NO_CONTENT) - .location(URI.create("/api/backoffice/recipes")) + .location(URI.create(ADMIN_RECIPE_PATH)) .build(); } } diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProductRepository.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java similarity index 64% rename from src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProductRepository.java rename to src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java index 623c89d..f500379 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java @@ -1,5 +1,6 @@ -package com.example.purebasketbe.domain.recipe.entity; +package com.example.purebasketbe.domain.recipe; +import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java index e041075..cfc2603 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java @@ -8,10 +8,8 @@ import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; -import io.awspring.cloud.s3.ObjectMetadata; -import io.awspring.cloud.s3.S3Template; +import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -19,10 +17,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; -import java.io.InputStream; import java.util.List; -import java.util.UUID; @Service @RequiredArgsConstructor @@ -30,12 +25,8 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final ProductRepository productRepository; - private final S3Template s3Template; + private final S3Handler s3Handler; - @Value("${aws.bucket.name}") - private String bucket; - @Value("${spring.cloud.aws.region.static}") - private String region; private final int RECIPES_PER_PAGE = 10; @Transactional(readOnly = true) @@ -60,8 +51,7 @@ public RecipeResponseDto getRecipe(Long recipeId) { @Transactional public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { checkExistRecipeByName(requestDto); - String key = UUID.randomUUID() + "_" + file.getOriginalFilename(); - String imgUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key); + String imgUrl = s3Handler.makeUrl(file); Recipe recipe = Recipe.from(requestDto, imgUrl); for (Long productId : requestDto.getProductIdList()) { @@ -71,30 +61,16 @@ public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { } recipeRepository.save(recipe); - // S3 저장은 commit이 된 이후에 수행되어야 한다. - uploadImage(file, key); + s3Handler.uploadImages(imgUrl, file); } @Transactional - public void deleteProduct(Long recipeId) { + public void deleteRecipe(Long recipeId) { Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(() -> new CustomException(ErrorCode.RECIPE_NOT_FOUND) ); + s3Handler.deleteImage(recipe.getImgUrl()); recipeRepository.delete(recipe); - // ToDo: S3에서 사진 삭제하기 코드 추가 - } - - // ToDo: productService에 있는 메서드와 합친 후에 S3 관련 폴더 생성하기 - private void uploadImage(MultipartFile file, String key) { - InputStream inputStream; - try { - inputStream = file.getInputStream(); - } catch (IOException e) { - throw new CustomException(ErrorCode.INVALID_IMAGE); - } - ObjectMetadata metadata = ObjectMetadata.builder().contentType("text/plain").build(); - - s3Template.upload(bucket, key, inputStream, metadata); } private Product findValidProduct(Long productId) { diff --git a/src/main/java/com/example/purebasketbe/global/s3/S3Handler.java b/src/main/java/com/example/purebasketbe/global/s3/S3Handler.java new file mode 100644 index 0000000..7cb69c0 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/s3/S3Handler.java @@ -0,0 +1,67 @@ +package com.example.purebasketbe.global.s3; + + +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import io.awspring.cloud.s3.ObjectMetadata; +import io.awspring.cloud.s3.S3Template; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class S3Handler { + + private final S3Template s3Template; + + @Value("${aws.bucket.name}") + private String bucket; + @Value("${spring.cloud.aws.region.static}") + private String region; + + public String makeUrl(MultipartFile file) { + String key = getRandomKey(file); + return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key); + } + + public void uploadImages(String imgUrl, MultipartFile file) { + String key = getKey(imgUrl); + InputStream inputStream; + try { + inputStream = file.getInputStream(); + } catch (IOException e) { + throw new CustomException(ErrorCode.INVALID_IMAGE); + } + ObjectMetadata metadata = ObjectMetadata.builder().contentType("text/plain").build(); + + s3Template.upload(bucket, key, inputStream, metadata); + } + + public void uploadImages(List imgUrlList, List files) { + for (int i = 0; i < imgUrlList.size(); i++) { + uploadImages(imgUrlList.get(i), files.get(i)); + } + } + + public void deleteImage(String imgUrl) { + String key = getKey(imgUrl); + s3Template.deleteObject(bucket, key); + } + + private String getRandomKey(MultipartFile file) { + return UUID.randomUUID() + "_" + file.getOriginalFilename(); + } + private String getKey(String imgUrl) { + int lastIndex = imgUrl.lastIndexOf("/"); + if (lastIndex != -1) { + return imgUrl.substring(lastIndex + 1); + } else throw new CustomException(ErrorCode.INVALID_IMAGE); + } +} From b70e49a5fd0af9901a44897d7f951616e30b58c7 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 9 Dec 2023 17:00:24 +0900 Subject: [PATCH 08/82] =?UTF-8?q?Remove:=20RecipeProduct=20entity=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/cart/CartService.java | 3 +- .../recipe/RecipeProductRepository.java | 10 ----- .../domain/recipe/RecipeService.java | 7 +-- .../domain/recipe/entity/Recipe.java | 22 ++++++---- .../domain/recipe/entity/RecipeProduct.java | 43 ------------------- 5 files changed, 16 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java delete mode 100644 src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProduct.java diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index 0b21073..b26d42d 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -10,7 +10,6 @@ import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.recipe.RecipeRepository; import com.example.purebasketbe.domain.recipe.entity.Recipe; -import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; @@ -63,7 +62,7 @@ public void addRecipeRelatedProductsToCarts(Long recipeId, Member member) { new CustomException(ErrorCode.RECIPE_NOT_FOUND) ); - List productList = recipe.getRecipeProductList().stream().map(RecipeProduct::getProduct).toList(); + List productList = recipe.getProductList(); cartRepository.deleteAllByMemberAndProductIn(member, productList); List cartList = productList.stream().map(product -> Cart.of(product, member, null)).toList(); cartRepository.saveAll(cartList); diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java deleted file mode 100644 index f500379..0000000 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeProductRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.purebasketbe.domain.recipe; - -import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface RecipeProductRepository extends JpaRepository { - -} diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java index cfc2603..b7e0611 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java @@ -5,7 +5,6 @@ import com.example.purebasketbe.domain.recipe.dto.RecipeRequestDto; import com.example.purebasketbe.domain.recipe.dto.RecipeResponseDto; import com.example.purebasketbe.domain.recipe.entity.Recipe; -import com.example.purebasketbe.domain.recipe.entity.RecipeProduct; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.s3.S3Handler; @@ -42,8 +41,7 @@ public Page getRecipes(int page) { public RecipeResponseDto getRecipe(Long recipeId) { Recipe recipe = recipeRepository.findById(recipeId).orElseThrow( () -> new CustomException(ErrorCode.RECIPE_NOT_FOUND)); - List productList = recipe.getRecipeProductList().stream() - .map(RecipeProduct::getProduct).toList(); + List productList = recipe.getProductList(); return RecipeResponseDto.of(recipe, productList); } @@ -56,8 +54,7 @@ public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { Recipe recipe = Recipe.from(requestDto, imgUrl); for (Long productId : requestDto.getProductIdList()) { Product product = findValidProduct(productId); - RecipeProduct recipeProduct = RecipeProduct.of(recipe, product); - recipe.addRecipeProduct(recipeProduct); + recipe.addProduct(product); } recipeRepository.save(recipe); diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java b/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java index 09dc0bd..7426a87 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java @@ -1,5 +1,6 @@ package com.example.purebasketbe.domain.recipe.entity; +import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.recipe.dto.RecipeRequestDto; import jakarta.persistence.*; import lombok.AccessLevel; @@ -28,16 +29,20 @@ public class Recipe { @Column private String imgUrl; - @OneToMany(mappedBy = "recipe", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) - @OrderBy("product.id asc") - private List recipeProductList; + + @ManyToMany + @JoinTable(name = "recipe_product", + joinColumns = @JoinColumn(name = "recipe_id"), + inverseJoinColumns = @JoinColumn(name = "product_id")) + private List productList = new ArrayList<>(); + @Builder - private Recipe(String name, String info, String imgUrl, List recipeProductList) { + private Recipe(String name, String info, String imgUrl, List productList) { this.name = name; this.info = info; this.imgUrl = imgUrl; - this.recipeProductList = recipeProductList; + this.productList = productList; } public static Recipe from(RecipeRequestDto requestDto, String imgUrl) { @@ -45,13 +50,12 @@ public static Recipe from(RecipeRequestDto requestDto, String imgUrl) { .name(requestDto.getName()) .info(requestDto.getInfo()) .imgUrl(imgUrl) - .recipeProductList(new ArrayList<>()) + .productList(new ArrayList<>()) .build(); } - public void addRecipeProduct(RecipeProduct recipeProduct) { - this.recipeProductList.add(recipeProduct); - recipeProduct.addRecipe(this); + public void addProduct(Product product) { + this.productList.add(product); } } diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProduct.java b/src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProduct.java deleted file mode 100644 index b48d564..0000000 --- a/src/main/java/com/example/purebasketbe/domain/recipe/entity/RecipeProduct.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.example.purebasketbe.domain.recipe.entity; - -import com.example.purebasketbe.domain.product.entity.Product; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "recipe_product") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RecipeProduct { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "recipe_id") - private Recipe recipe; - - @ManyToOne - @JoinColumn(name = "product_id") - private Product product; - - @Builder - private RecipeProduct(Recipe recipe, Product product) { - this.recipe = recipe; - this.product = product; - } - - public static RecipeProduct of(Recipe recipe, Product product) { - return RecipeProduct.builder() - .recipe(recipe) - .product(product) - .build(); - } - - public void addRecipe(Recipe recipe) { - this.recipe = recipe; - } -} From 026f09948d5e874f87ac790179046946bfd34539 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 11 Dec 2023 11:56:05 +0900 Subject: [PATCH 09/82] =?UTF-8?q?Refactor:=20Product=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductController.java | 6 +- .../domain/product/ProductService.java | 68 +++++++++---------- .../domain/product/dto/ImageResponseDto.java | 31 --------- .../product/dto/ProductDetailResponseDto.java | 27 -------- .../product/dto/ProductListResponseDto.java | 14 +--- .../domain/product/dto/ProductRequestDto.java | 1 - .../product/dto/ProductResponseDto.java | 23 +++++-- .../domain/product/entity/Product.java | 8 +-- 8 files changed, 59 insertions(+), 119 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/domain/product/dto/ImageResponseDto.java delete mode 100644 src/main/java/com/example/purebasketbe/domain/product/dto/ProductDetailResponseDto.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java index b8e7174..9149cc0 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java @@ -1,7 +1,7 @@ package com.example.purebasketbe.domain.product; import com.example.purebasketbe.domain.product.dto.ProductListResponseDto; -import com.example.purebasketbe.domain.product.dto.ProductDetailResponseDto; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -33,8 +33,8 @@ public ResponseEntity searchProducts( } @GetMapping("/{productId}") - public ResponseEntity getProduct(@PathVariable Long productId) { - ProductDetailResponseDto responseBody = productService.getProduct(productId); + public ResponseEntity getProduct(@PathVariable Long productId) { + ProductResponseDto responseBody = productService.getProduct(productId); return ResponseEntity.status(HttpStatus.OK).body(responseBody); } } diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 95b3500..58314d5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -32,16 +32,15 @@ public class ProductService { @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { Pageable eventPageable = getPageable(eventPage, eventPageSize); - Page eventProducts = productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, eventPageable); - Page eventProductsResponse = eventProducts.map(ProductResponseDto::from); - List eventImageUrlResponse = getImageUrlResponse(eventProducts); - Pageable pageable = getPageable(page, pageSize); + + Page eventProducts = productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, eventPageable); Page products = productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageable); - Page productsResponse = products.map(ProductResponseDto::from); - List imageUrlResponse = getImageUrlResponse(products); - return ProductListResponseDto.of(eventProductsResponse, eventImageUrlResponse, productsResponse, imageUrlResponse); + Page eventProductsResponse = getResponseDtoFromProducts(eventProducts); + Page productsResponse = getResponseDtoFromProducts(products); + + return ProductListResponseDto.of(eventProductsResponse, productsResponse); } @Transactional(readOnly = true) @@ -49,32 +48,31 @@ public ProductListResponseDto searchProducts(String query, String category, int Pageable eventPageable = getPageable(eventPage, eventPageSize); Pageable pageable = getPageable(page, pageSize); - Page eventProducts; - Page products; - if (category.isEmpty()) { - eventProducts = productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.DISCOUNT, query, eventPageable); - products = productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.NORMAL, query, pageable); - } else { - eventProducts = productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.DISCOUNT, category, query, eventPageable); - products = productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.NORMAL, category, query, pageable); - } - Page eventProductsResponse = eventProducts.map(ProductResponseDto::from); - List eventImageUrlResponse = getImageUrlResponse(eventProducts); - Page productsResponse = products.map(ProductResponseDto::from); - List imageUrlResponse = getImageUrlResponse(products); - return ProductListResponseDto.of(eventProductsResponse, eventImageUrlResponse, productsResponse, imageUrlResponse); + Page eventProducts = category.isEmpty() + ? productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.DISCOUNT, query, eventPageable) + : productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.DISCOUNT, category, query, eventPageable); + + Page products = category.isEmpty() + ? productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.NORMAL, query, pageable) + : productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.NORMAL, category, query, pageable); + + Page eventProductsResponse = getResponseDtoFromProducts(eventProducts); + Page productsResponse = getResponseDtoFromProducts(products); + + return ProductListResponseDto.of(eventProductsResponse, productsResponse); } + @Transactional(readOnly = true) - public ProductDetailResponseDto getProduct(Long productId) { + public ProductResponseDto getProduct(Long productId) { Product product = findProduct(productId); List imageUrlList = getImgUrlList(product); - return ProductDetailResponseDto.of(product, imageUrlList); + return ProductResponseDto.of(product, imageUrlList); } @Transactional @@ -105,20 +103,18 @@ public void deleteProduct(Long productId) { imageRepository.deleteAllByProductId(productId); } + private Page getResponseDtoFromProducts(Page products) { + return products.map(product -> { + List imgUrlList = getImgUrlList(product); + return ProductResponseDto.of(product, imgUrlList); + }); + } + private Pageable getPageable(int page, int pageSize) { Sort sort = Sort.by(Sort.Direction.DESC, "modifiedAt"); return PageRequest.of(page, pageSize, sort); } - private List getImageUrlResponse(Page products) { - List imageUrlResponse = new ArrayList<>(); - for (Product product : products) { - List imgUrlList = getImgUrlList(product); - imageUrlResponse.add(ImageResponseDto.of(product, imgUrlList)); - } - return imageUrlResponse; - } - private List getImgUrlList(Product product) { return imageRepository.findAllByProductId(product.getId()) .stream() diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ImageResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ImageResponseDto.java deleted file mode 100644 index acc49f6..0000000 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ImageResponseDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.purebasketbe.domain.product.dto; - -import com.example.purebasketbe.domain.product.entity.Product; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ImageResponseDto { - - private Long productId; - - private List imgUrlList; - - @Builder - private ImageResponseDto(Long productId, List imgUrlList) { - this.productId = productId; - this.imgUrlList = imgUrlList; - } - - public static ImageResponseDto of(Product product, List imgUrlList) { - return ImageResponseDto.builder() - .productId(product.getId()) - .imgUrlList(imgUrlList) - .build(); - } -} diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductDetailResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductDetailResponseDto.java deleted file mode 100644 index 6fc3627..0000000 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductDetailResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.purebasketbe.domain.product.dto; - -import com.example.purebasketbe.domain.product.entity.Product; -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Getter -@JsonInclude(JsonInclude.Include.NON_NULL) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProductDetailResponseDto extends ProductResponseDto { - - private List imgUrlList; - - private ProductDetailResponseDto(Product product, List imgUrlList) { - super(product.getId(), product.getName(), product.getPrice(), product.getStock(), - product.getInfo(), product.getCategory(), product.getEvent(), product.getDiscountRate()); - this.imgUrlList = imgUrlList; - } - - public static ProductDetailResponseDto of(Product product, List imgUrlList) { - return new ProductDetailResponseDto(product, imgUrlList); - } -} diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java index e15d65f..0ebd29d 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java @@ -13,31 +13,21 @@ public class ProductListResponseDto { private Page eventProducts; - private List eventImageUrls; private Page products; - private List imageUrls; @Builder private ProductListResponseDto(Page eventProducts, - List eventImageUrls, - Page products, - List imageUrls) { + Page products) { this.eventProducts = eventProducts; - this.eventImageUrls = eventImageUrls; this.products = products; - this.imageUrls = imageUrls; } public static ProductListResponseDto of(Page eventProducts, - List eventImageUrls, - Page products, - List imageUrls) { + Page products) { return ProductListResponseDto.builder() .eventProducts(eventProducts) - .eventImageUrls(eventImageUrls) .products(products) - .imageUrls(imageUrls) .build(); } } diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java index 2a41e18..f43da61 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java @@ -23,7 +23,6 @@ public record ProductRequestDto( String category, - @Enumerated(EnumType.STRING) Event event, @Min(value = 0, message = "할인율은 0 이상으로 입력해 주세요.") diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java index b8f233f..ff91d9d 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java @@ -8,6 +8,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.List; + @Getter @JsonInclude(JsonInclude.Include.NON_NULL) @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -15,16 +17,17 @@ public class ProductResponseDto { private Long id; private String name; - private Integer price; - private Integer stock; + private int price; + private int stock; private String info; private String category; private Event event; - private Integer discountRate; + private int discountRate; + private List images; @Builder - ProductResponseDto(Long id, String name, Integer price, Integer stock, String info, - String category, Event event, Integer discountRate) { + ProductResponseDto(Long id, String name, int price, int stock, String info, + String category, Event event, int discountRate) { this.id = id; this.name = name; this.price = price; @@ -47,4 +50,14 @@ public static ProductResponseDto from(Product product) { .discountRate(product.getDiscountRate()) .build(); } + + public static ProductResponseDto of(Product product, List imgUrlLsit) { + ProductResponseDto productResponseDto = ProductResponseDto.from(product); + productResponseDto.addImgUrls(imgUrlLsit); + return productResponseDto; + } + + private void addImgUrls(List imgUrlList) { + this.images = imgUrlList; + } } diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index 0d63cf5..daf753c 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -28,10 +28,10 @@ public class Product { private String name; @Column(nullable = false) - private Integer price; + private int price; @Column(nullable = false) - private Integer stock; + private int stock; private String info; @@ -40,9 +40,9 @@ public class Product { @Enumerated(EnumType.STRING) private Event event; - private Integer discountRate; + private int discountRate; - private Integer salesCount; + private int salesCount; @CreatedDate private LocalDateTime createdAt; From cd357757cf73fc2075d3a5a6b609fb0526205f1b Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Mon, 11 Dec 2023 16:23:38 +0900 Subject: [PATCH 10/82] =?UTF-8?q?!HOTFIX:=20name=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/member/dto/SignupRequestDto.java | 3 +-- .../purebasketbe/domain/member/entity/Member.java | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java b/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java index b96118a..f0c1e3c 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java @@ -18,6 +18,5 @@ public record SignupRequestDto ( String address, @NotBlank(message = "전화번호를 입력해주세요") @Pattern(regexp = "^[0-9]{10,11}$", message = "전화번호는 10~11자리의 숫자이어야 합니다") - String phone, - boolean deleted + String phone ){ } diff --git a/src/main/java/com/example/purebasketbe/domain/member/entity/Member.java b/src/main/java/com/example/purebasketbe/domain/member/entity/Member.java index 031918a..65a6ddd 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/entity/Member.java +++ b/src/main/java/com/example/purebasketbe/domain/member/entity/Member.java @@ -1,6 +1,7 @@ package com.example.purebasketbe.domain.member.entity; import com.example.purebasketbe.domain.member.dto.SignupRequestDto; +import com.example.purebasketbe.global.tool.LoginAccount; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -17,6 +18,9 @@ public class Member { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(nullable = false) + private String name; + @Column(nullable = false, unique = true) private String email; @@ -37,8 +41,9 @@ public class Member { private boolean deleted = false; @Builder - private Member(String email, String password, String phone, String address, UserRole role, boolean deleted){ + private Member(String email, String name, String password, String phone, String address, UserRole role, boolean deleted){ this.email = email; + this.name = name; this.password = password; this.phone = phone; this.address = address; @@ -47,12 +52,12 @@ private Member(String email, String password, String phone, String address, User public static Member of(SignupRequestDto requestDto, String password){ return Member.builder() + .name(requestDto.name()) .email(requestDto.email()) .password(password) .phone(requestDto.phone()) .address(requestDto.address()) .role(UserRole.MEMBER) - .deleted(requestDto.deleted()) .build(); } } \ No newline at end of file From 4399ea25f61fc5d52fcade5a9f88b84be4446033 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Mon, 11 Dec 2023 16:23:52 +0900 Subject: [PATCH 11/82] =?UTF-8?q?!HOTFIX:=20validated=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/domain/member/MemberController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberController.java b/src/main/java/com/example/purebasketbe/domain/member/MemberController.java index d885451..69eb208 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberController.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberController.java @@ -4,6 +4,7 @@ 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,7 +17,7 @@ public class MemberController { private final MemberService memberService; @PostMapping("/signup") - public ResponseEntity registerMember(@RequestBody SignupRequestDto requestDto) { + public ResponseEntity registerMember(@RequestBody @Validated SignupRequestDto requestDto) { memberService.registerMember(requestDto); return ResponseEntity.status(HttpStatus.CREATED).build(); } From b40c9e543432ec36fe2c70f45a4fc98c238e6e7d Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Mon, 11 Dec 2023 16:24:31 +0900 Subject: [PATCH 12/82] =?UTF-8?q?!HOTFIX:=20Jwt=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=ED=86=A0=ED=81=B0=EB=A7=8C=EB=A3=8C=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.java | 11 +---- .../filter/JwtAuthorizationFilter.java | 2 +- .../global/security/jwt/JwtUtil.java | 41 ++++--------------- 3 files changed, 11 insertions(+), 43 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 2ce8673..3fa7200 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -17,6 +17,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.util.StringUtils; import java.io.IOException; import java.time.Duration; @@ -72,24 +73,16 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String token = jwtUtil.createToken(email, role); - String refreshToken = jwtUtil.createRefreshToken(email, role); + String refreshToken = jwtUtil.createRefreshToken(email); jwtUtil.addJwtToHeader(JwtUtil.AUTHORIZATION_HEADER, token, response); jwtUtil.addJwtToHeader(JwtUtil.REFRESHTOKEN_HEADER,refreshToken,response); - // 리프레시 토큰 만료 여부 확인 - if (jwtUtil.isRefreshTokenExpired(refreshToken)) { - response.setContentType("application/json;charsetUTF=8"); - response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("재로그인 필요"))); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - } - // 로그인 성공시 Refresh Token Redis 저장 ( key = Email / value = Refresh Token ) UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); Member findMember = memberService.findMemberByEmail(userDetails.getUsername()); long refreshTokenExpirationMillis = jwtUtil.getRefreshTokenExpirationMillis(); redisService.setValues(findMember.getEmail(), refreshToken, Duration.ofMillis(refreshTokenExpirationMillis)); - // 성공 메시지와 토큰을 JSON 형식으로 응답에 추가 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("로그인 성공"))); diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthorizationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthorizationFilter.java index d9de3e4..af8afaf 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthorizationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthorizationFilter.java @@ -38,7 +38,7 @@ protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, try{ if (req.getHeader(JwtUtil.AUTHORIZATION_HEADER) == null) throw new CustomException(ErrorCode.NOT_FOUND_TOKEN); - String token = URLDecoder.decode(req.getHeader(JwtUtil.AUTHORIZATION_HEADER), StandardCharsets.UTF_8); + String token = URLDecoder.decode(req.getHeader(JwtUtil.AUTHORIZATION_HEADER), "UTF-8"); if (StringUtils.hasText(token)) { diff --git a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java index 1c8c7a0..9ed8388 100644 --- a/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java +++ b/src/main/java/com/example/purebasketbe/global/security/jwt/JwtUtil.java @@ -4,23 +4,19 @@ import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import io.jsonwebtoken.*; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Encoders; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.security.Key; +import java.util.Base64; import java.util.Date; @Slf4j @@ -50,23 +46,11 @@ public class JwtUtil { private Key key; private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; - // 로그 설정 - public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그"); - // Bean 등록후 Key SecretKey HS256 decode @PostConstruct public void init() { - String base64EncodedSecretKey = encodeBase64SecretKey(this.secretKey); - this.key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); - } - - public String encodeBase64SecretKey(String secretKey) { - return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8)); - } - - private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { - byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); - return Keys.hmacShaKeyFor(keyBytes); + byte[] bytes = Base64.getDecoder().decode(secretKey); + key = Keys.hmacShaKeyFor(bytes); } // 토큰 생성 @@ -84,7 +68,7 @@ public String createToken(String username, UserRole role) { } // 리프레쉬 토큰 생성 - public String createRefreshToken(String username, UserRole role) { + public String createRefreshToken(String username) { Date date = new Date(); return BEARER_PREFIX + @@ -122,32 +106,23 @@ public boolean validateToken(String token) { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); } catch (MalformedJwtException e) { log.info("Invalid JWT token, 유효하지 않는 JWT 서명 입니다."); - log.trace("Invalid JWT token trace = {e}", e); + log.trace("Invalid JWT token trace = {}", e); } catch (ExpiredJwtException e) { log.info("Expired JWT token, 만료된 JWT token 입니다."); - log.trace("Expired JWT token trace = {e}", e); + log.trace("Expired JWT token trace = {}", e); throw new CustomException(ErrorCode.TOKEN_EXPIRED); } catch (UnsupportedJwtException e) { log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); - log.trace("Unsupported JWT token trace = {e}", e); + log.trace("Unsupported JWT token trace = {}", e); throw new CustomException(ErrorCode.TOKEN_UNSUPPORTED); } catch (IllegalArgumentException e) { log.info("JWT claims string is empty, 잘못된 JWT 토큰 입니다."); - log.trace("JWT claims string is empty trace = {e}", e); + log.trace("JWT claims string is empty trace = {}", e); throw new CustomException(ErrorCode.TOKEN_INVALID); } return true; } - public boolean isRefreshTokenExpired(String token) { - try { - Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); - return false; - } catch (ExpiredJwtException e) { - return true; - } - } - // 토큰에서 사용자 정보 가져오기 public Claims getUserInfoFromToken(String token) { return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); From b193a8d28772fdb801f9d59daf308bc3f9708290 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:14:41 +0900 Subject: [PATCH 13/82] =?UTF-8?q?Update:=20UserRole=20Owner=20->=20Admin?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/purebasketbe/domain/member/entity/UserRole.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/member/entity/UserRole.java b/src/main/java/com/example/purebasketbe/domain/member/entity/UserRole.java index b9e2158..0209ce0 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/entity/UserRole.java +++ b/src/main/java/com/example/purebasketbe/domain/member/entity/UserRole.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor public enum UserRole { MEMBER(Authority.MEMBER), - OWNER(Authority.ADMIN); + ADMIN(Authority.ADMIN); private final String authority; From 1dcb63217591b57d3b9778dfdc3142054c7a3ff2 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:15:31 +0900 Subject: [PATCH 14/82] =?UTF-8?q?Update:=20Admin=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20Role=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/admin/entity/Admin.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/admin/entity/Admin.java b/src/main/java/com/example/purebasketbe/domain/admin/entity/Admin.java index dfe1989..7df13b7 100644 --- a/src/main/java/com/example/purebasketbe/domain/admin/entity/Admin.java +++ b/src/main/java/com/example/purebasketbe/domain/admin/entity/Admin.java @@ -1,11 +1,7 @@ package com.example.purebasketbe.domain.admin.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import com.example.purebasketbe.domain.member.entity.UserRole; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -24,4 +20,8 @@ public class Admin { @Column(nullable = false) private String password; + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + private final UserRole role = UserRole.ADMIN; } From e0ab3335fba14393e2f67f795bd9d942e1f31c03 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:15:58 +0900 Subject: [PATCH 15/82] =?UTF-8?q?Feat:=20Admin=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/admin/AdminRepository.java | 10 ++++++ .../domain/admin/AuthController.java | 35 +++++++++++++++++++ .../domain/admin/AuthService.java | 22 ++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/main/java/com/example/purebasketbe/domain/admin/AdminRepository.java create mode 100644 src/main/java/com/example/purebasketbe/domain/admin/AuthController.java create mode 100644 src/main/java/com/example/purebasketbe/domain/admin/AuthService.java diff --git a/src/main/java/com/example/purebasketbe/domain/admin/AdminRepository.java b/src/main/java/com/example/purebasketbe/domain/admin/AdminRepository.java new file mode 100644 index 0000000..d077dcc --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/admin/AdminRepository.java @@ -0,0 +1,10 @@ +package com.example.purebasketbe.domain.admin; + +import com.example.purebasketbe.domain.admin.entity.Admin; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AdminRepository extends JpaRepository { + Optional findByEmail(String email); +} diff --git a/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java b/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java new file mode 100644 index 0000000..2c5e238 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java @@ -0,0 +1,35 @@ +package com.example.purebasketbe.domain.admin; + +import com.example.purebasketbe.domain.member.dto.LoginRequestDto; +import com.example.purebasketbe.domain.member.entity.UserRole; +import com.example.purebasketbe.global.security.jwt.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +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; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/auth") +public class AuthController { + private final JwtUtil jwtUtil; + private final AuthService authService; + + @PostMapping("/admin/login") + public ResponseEntity adminLogin(@RequestBody LoginRequestDto requestDto) { + // 관리자 인증 서비스로 로그인 처리 + Authentication authentication = authService.authenticateAdmin(requestDto); + // JWT 토큰 생성 + String token = jwtUtil.createToken(authentication.getName(), UserRole.ADMIN); + // 토큰을 응답 헤더에 추가 + HttpHeaders headers = new HttpHeaders(); + headers.add(JwtUtil.AUTHORIZATION_HEADER, token); + // 성공 메시지와 함께 응답 반환 + return ResponseEntity.status(HttpStatus.OK).build(); + } +} diff --git a/src/main/java/com/example/purebasketbe/domain/admin/AuthService.java b/src/main/java/com/example/purebasketbe/domain/admin/AuthService.java new file mode 100644 index 0000000..a0b767e --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/admin/AuthService.java @@ -0,0 +1,22 @@ +package com.example.purebasketbe.domain.admin; + +import com.example.purebasketbe.domain.member.dto.LoginRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final AuthenticationManager authenticationManager; + + public Authentication authenticateAdmin(LoginRequestDto requestDto) { + // 이메일과 비밀번호를 사용하여 UsernamePasswordAuthenticationToken 생성 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password()); + // AuthenticationManager를 사용하여 인증 시도 후 인증 성공 시, Authentication 객체 반환 + return authenticationManager.authenticate(authenticationToken); + } +} From 01334f05536e52c73eec4fb3b1f07d24c6135973 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:16:19 +0900 Subject: [PATCH 16/82] =?UTF-8?q?Feat:=20LoginRequestDto=20validation?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/member/dto/LoginRequestDto.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java b/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java index 8f56153..e62fa90 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java @@ -1,6 +1,13 @@ package com.example.purebasketbe.domain.member.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + public record LoginRequestDto ( + @NotBlank(message = "이메일을 입력해주세요") + @Email(message = "형식에 맞게 입력해주세요") String email, + + @NotBlank(message = "비밀번호를 입력해주세요") String password ) { } From 665fb4c2a70816a866cc8f1eb87d8413c513ca2c Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:16:48 +0900 Subject: [PATCH 17/82] =?UTF-8?q?Update:=20Admin=20login=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/ErrorCode.java | 1 + .../filter/JwtAuthenticationFilter.java | 7 +++-- .../global/security/impl/UserDetailsImpl.java | 31 +++++++++++++++---- .../security/impl/UserDetailsServiceImpl.java | 21 ++++++++++--- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java index f8c3b0d..46d41d9 100644 --- a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java +++ b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java @@ -49,6 +49,7 @@ public enum ErrorCode { ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST.value(), "포인트가 부족합니다."), ORDER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "처리할 주문이 없습니다."), + UNSUPPORTED_USER_TYPE(HttpStatus.BAD_REQUEST.value(), "잘못된 사용자 유형입니다."), UNEXPECTED_ERROR(443, "예상치 못한 오류가 발생했습니다."),; private final int httpStatus; diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 3fa7200..5a428bf 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -16,11 +16,13 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.util.StringUtils; import java.io.IOException; import java.time.Duration; +import java.util.Collection; @Slf4j(topic = "로그인 및 JWT 생성") public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @@ -68,7 +70,9 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR log.info("로그인 성공 및 JWT 생성"); String email = ((UserDetailsImpl) authResult.getPrincipal()).getUsername(); - UserRole role = ((UserDetailsImpl) authResult.getPrincipal()).getMember().getRole(); + UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); + Collection authorities = userDetails.getAuthorities(); + UserRole role = UserRole.valueOf(authorities.iterator().next().getAuthority()); @@ -78,7 +82,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR jwtUtil.addJwtToHeader(JwtUtil.REFRESHTOKEN_HEADER,refreshToken,response); // 로그인 성공시 Refresh Token Redis 저장 ( key = Email / value = Refresh Token ) - UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); Member findMember = memberService.findMemberByEmail(userDetails.getUsername()); long refreshTokenExpirationMillis = jwtUtil.getRefreshTokenExpirationMillis(); redisService.setValues(findMember.getEmail(), refreshToken, Duration.ofMillis(refreshTokenExpirationMillis)); diff --git a/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsImpl.java b/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsImpl.java index ea643be..9bdc252 100644 --- a/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsImpl.java +++ b/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsImpl.java @@ -1,6 +1,9 @@ package com.example.purebasketbe.global.security.impl; +import com.example.purebasketbe.domain.admin.entity.Admin; import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.security.core.GrantedAuthority; @@ -14,22 +17,38 @@ @AllArgsConstructor public class UserDetailsImpl implements UserDetails { - private final Member member; + private final Object user; @Override public String getPassword() { - return member.getPassword(); + if (user instanceof Member) { + return ((Member) user).getPassword(); + } else if (user instanceof Admin) { + return ((Admin) user).getPassword(); + } + throw new CustomException(ErrorCode.UNSUPPORTED_USER_TYPE); } @Override - public String getUsername() { // 이메일 반환 - return member.getEmail(); + public String getUsername() { + if (user instanceof Member) { + return ((Member) user).getEmail(); + } else if (user instanceof Admin) { + return ((Admin) user).getEmail(); + } + throw new CustomException(ErrorCode.UNSUPPORTED_USER_TYPE); } @Override public Collection getAuthorities() { - String authority = member.getRole().getAuthority(); - return Collections.singletonList(new SimpleGrantedAuthority(authority)); + if (user instanceof Member) { + String authority = ((Member) user).getRole().getAuthority(); + return Collections.singletonList(new SimpleGrantedAuthority(authority)); + } else if (user instanceof Admin) { + String authority = ((Admin) user).getRole().getAuthority(); + return Collections.singletonList(new SimpleGrantedAuthority(authority)); + } + throw new CustomException(ErrorCode.UNSUPPORTED_USER_TYPE); } @Override diff --git a/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsServiceImpl.java b/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsServiceImpl.java index 67da6ef..038dbee 100644 --- a/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsServiceImpl.java +++ b/src/main/java/com/example/purebasketbe/global/security/impl/UserDetailsServiceImpl.java @@ -1,5 +1,7 @@ package com.example.purebasketbe.global.security.impl; +import com.example.purebasketbe.domain.admin.AdminRepository; +import com.example.purebasketbe.domain.admin.entity.Admin; import com.example.purebasketbe.domain.member.MemberRepository; import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.global.exception.CustomException; @@ -10,17 +12,28 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import java.util.Optional; + @Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final MemberRepository memberRepository; + private final AdminRepository adminRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.INVALID_EMAIL_PASSWORD)); - - return new UserDetailsImpl(member); + // 먼저 관리자 Repository에서 이메일로 관리자 정보 조회 시도 + Optional admin = adminRepository.findByEmail(email); + if (admin.isPresent()) { + // 관리자 정보가 존재하는 경우 UserDetailsImpl 객체 생성하여 반환 + return new UserDetailsImpl(admin.get()); + } else { + // 관리자 정보가 없다면, 일반 사용자 Repository에서 조회 + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.INVALID_EMAIL_PASSWORD)); + // 일반 사용자 정보로 UserDetailsImpl 객체 생성하여 반환 + return new UserDetailsImpl(member); + } } } \ No newline at end of file From fe3079a1b7fbd608af90642b4f126cd54822b851 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 15:54:11 +0900 Subject: [PATCH 18/82] =?UTF-8?q?!HOTFIX:=20JwtAuthenticationFilter?= =?UTF-8?q?=EC=9D=98=20UserRole=20=EA=B6=8C=ED=95=9C=20=EB=AC=B8=EC=9E=90?= =?UTF-8?q?=EC=97=B4=20=EC=B2=98=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserRole 권한 문자열 파싱을 ROLE_ 접두사를 올바르게 처리하도록 수정. - UserRole 열거형 상수 불일치로 발생한 IllegalArgumentException 문제 해결. - 인증 과정에서 권한 문자열에 따른 UserRole 할당을 보장. --- .../global/security/filter/JwtAuthenticationFilter.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java index 5a428bf..0b6ab57 100644 --- a/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/purebasketbe/global/security/filter/JwtAuthenticationFilter.java @@ -72,9 +72,8 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR String email = ((UserDetailsImpl) authResult.getPrincipal()).getUsername(); UserDetailsImpl userDetails = (UserDetailsImpl) authResult.getPrincipal(); Collection authorities = userDetails.getAuthorities(); - UserRole role = UserRole.valueOf(authorities.iterator().next().getAuthority()); - - + String authority = authorities.iterator().next().getAuthority(); + UserRole role = UserRole.valueOf(authority.replace("ROLE_", "")); String token = jwtUtil.createToken(email, role); String refreshToken = jwtUtil.createRefreshToken(email); From 4334a3498873a240b4d09119fbf35954b2ed4a10 Mon Sep 17 00:00:00 2001 From: Seoyoon Date: Tue, 12 Dec 2023 16:19:40 +0900 Subject: [PATCH 19/82] =?UTF-8?q?!HotFix:=20AuthController=EC=97=90=20retu?= =?UTF-8?q?rn=EA=B0=92=20headers=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/purebasketbe/domain/admin/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java b/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java index 2c5e238..15ddbe7 100644 --- a/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java +++ b/src/main/java/com/example/purebasketbe/domain/admin/AuthController.java @@ -30,6 +30,6 @@ public ResponseEntity adminLogin(@RequestBody LoginRequestDto requestDto) HttpHeaders headers = new HttpHeaders(); headers.add(JwtUtil.AUTHORIZATION_HEADER, token); // 성공 메시지와 함께 응답 반환 - return ResponseEntity.status(HttpStatus.OK).build(); + return ResponseEntity.status(HttpStatus.OK).headers(headers).build(); } } From d5e76237f11bb3d922a5ab956c7a59008e53705f Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 13 Dec 2023 11:20:34 +0900 Subject: [PATCH 20/82] =?UTF-8?q?Update:=20PRODUCTS=5FPER=5FPAGE=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/domain/purchase/PurchaseService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 4986eea..373c6c6 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -30,7 +30,7 @@ public class PurchaseService { private final CartRepository cartRepository; private final ProductRepository productRepository; - private final int PRODUCTS_PER_PAGE = 20; + private final int PRODUCTS_PER_PAGE = 10; @Transactional public void purchaseProducts(final List purchaseRequestDto, Member member) { From b0743587fb5bb570429b528b6664d1ec94aecf77 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 13 Dec 2023 11:22:52 +0900 Subject: [PATCH 21/82] =?UTF-8?q?!HOTFIX:=20=EC=9C=A0=EC=A0=80=20Authentic?= =?UTF-8?q?ationPrincipal=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @LoginAccount expression member에서 user로 변경 --- .../java/com/example/purebasketbe/global/tool/LoginAccount.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java b/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java index adb30c9..540dccc 100644 --- a/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java +++ b/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java @@ -9,6 +9,6 @@ @SuppressWarnings("ALL") @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) -@AuthenticationPrincipal(expression = "member") +@AuthenticationPrincipal(expression = "user") public @interface LoginAccount { } From b484932369e811db177cb14e567dd35fba5d5170 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 14 Dec 2023 13:48:21 +0900 Subject: [PATCH 22/82] =?UTF-8?q?Fix:=20addCart=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상품 장바구니 추가 시 기존에 등록된 상품인 경우 exception 발생 --- .../example/purebasketbe/domain/cart/CartRepository.java | 3 +++ .../example/purebasketbe/domain/cart/CartService.java | 9 +++++++++ .../example/purebasketbe/global/exception/ErrorCode.java | 3 ++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index 2f0d31d..cf932b2 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -14,6 +14,9 @@ @Repository public interface CartRepository extends JpaRepository { + @Query("select exists (select c.id from Cart c where c.product = :product)") + boolean existsProduct(Product product); + @Modifying @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") void deleteByUserAndProductIn(Member member, List products); diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index b26d42d..5ca2073 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -13,11 +13,13 @@ import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class CartService { @@ -30,6 +32,7 @@ public class CartService { @Transactional public void addToCart(Long productId, CartRequestDto requestDto, Member member) { Product product = findAndValidateProduct(productId); + chcekDuplicate(product); Cart newCart = Cart.of(product, member, requestDto); cartRepository.save(newCart); } @@ -78,6 +81,12 @@ private Product findAndValidateProduct(Long productId) { return product; } + private void chcekDuplicate(Product product) { + if (cartRepository.existsProduct(product)) { + throw new CustomException(ErrorCode.PRODUCT_ALREADY_ADDED); + } + } + private Cart findAndValidateCart(Long productId, Member member) { return cartRepository.findByProductIdAndMember(productId, member).orElseThrow( () -> new CustomException(ErrorCode.INVALID_CART_ITEM) diff --git a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java index 46d41d9..0abc85e 100644 --- a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java +++ b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java @@ -27,6 +27,7 @@ public enum ErrorCode { ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 사용자를 찾을 수 없습니다."), INVALID_CART_ITEM(HttpStatus.BAD_REQUEST.value(), "장바구니에 없는 상품입니다."), + PRODUCT_ALREADY_ADDED(HttpStatus.BAD_REQUEST.value(), "이미 장바구니에 등록된 상품입니다."), PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 상품입니다."), NOT_ENOUGH_PRODUCT(HttpStatus.NOT_FOUND.value(), "상품 재고가 부족합니다."), @@ -50,7 +51,7 @@ public enum ErrorCode { INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST.value(), "포인트가 부족합니다."), ORDER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "처리할 주문이 없습니다."), UNSUPPORTED_USER_TYPE(HttpStatus.BAD_REQUEST.value(), "잘못된 사용자 유형입니다."), - UNEXPECTED_ERROR(443, "예상치 못한 오류가 발생했습니다."),; + UNEXPECTED_ERROR(443, "예상치 못한 오류가 발생했습니다."); private final int httpStatus; private final String message; From 436c903ddc7c13b66203b2eefeb0b057f5a676eb Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 14 Dec 2023 13:48:21 +0900 Subject: [PATCH 23/82] =?UTF-8?q?Fix:=20addCart=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상품 장바구니 추가 시 기존에 등록된 상품인 경우 exception 발생 --- .../example/purebasketbe/domain/cart/CartRepository.java | 3 +++ .../example/purebasketbe/domain/cart/CartService.java | 9 +++++++++ .../example/purebasketbe/global/exception/ErrorCode.java | 5 +++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index 2f0d31d..cf932b2 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -14,6 +14,9 @@ @Repository public interface CartRepository extends JpaRepository { + @Query("select exists (select c.id from Cart c where c.product = :product)") + boolean existsProduct(Product product); + @Modifying @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") void deleteByUserAndProductIn(Member member, List products); diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index b26d42d..5ca2073 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -13,11 +13,13 @@ import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class CartService { @@ -30,6 +32,7 @@ public class CartService { @Transactional public void addToCart(Long productId, CartRequestDto requestDto, Member member) { Product product = findAndValidateProduct(productId); + chcekDuplicate(product); Cart newCart = Cart.of(product, member, requestDto); cartRepository.save(newCart); } @@ -78,6 +81,12 @@ private Product findAndValidateProduct(Long productId) { return product; } + private void chcekDuplicate(Product product) { + if (cartRepository.existsProduct(product)) { + throw new CustomException(ErrorCode.PRODUCT_ALREADY_ADDED); + } + } + private Cart findAndValidateCart(Long productId, Member member) { return cartRepository.findByProductIdAndMember(productId, member).orElseThrow( () -> new CustomException(ErrorCode.INVALID_CART_ITEM) diff --git a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java index 46d41d9..3495be2 100644 --- a/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java +++ b/src/main/java/com/example/purebasketbe/global/exception/ErrorCode.java @@ -11,7 +11,7 @@ public enum ErrorCode { EMAIL_DIFFERENT_FORMAT(HttpStatus.BAD_REQUEST.value(), "이메일 형식이 올바르지 않습니다."), EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "이메일을 찾을 수 없습니다"), PASSWORD_DIFFERENT_FORMAT(HttpStatus.BAD_REQUEST.value(), "비밀번호는 8~15자리로, 알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다."), - PRODUCT_ALREADY_EXISTS(HttpStatus.CONTINUE.value(), "이미 등록된 물건입니다."), + PRODUCT_ALREADY_EXISTS(HttpStatus.BAD_REQUEST.value(), "이미 등록된 물건입니다."), EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT.value(), "이미 등록된 이메일입니다."), INVALID_EMAIL_PASSWORD(HttpStatus.BAD_REQUEST.value(), "이메일 또는 비밀번호가 정확하지 않습니다."), @@ -27,6 +27,7 @@ public enum ErrorCode { ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 사용자를 찾을 수 없습니다."), INVALID_CART_ITEM(HttpStatus.BAD_REQUEST.value(), "장바구니에 없는 상품입니다."), + PRODUCT_ALREADY_ADDED(HttpStatus.BAD_REQUEST.value(), "이미 장바구니에 등록된 상품입니다."), PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 상품입니다."), NOT_ENOUGH_PRODUCT(HttpStatus.NOT_FOUND.value(), "상품 재고가 부족합니다."), @@ -50,7 +51,7 @@ public enum ErrorCode { INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST.value(), "포인트가 부족합니다."), ORDER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "처리할 주문이 없습니다."), UNSUPPORTED_USER_TYPE(HttpStatus.BAD_REQUEST.value(), "잘못된 사용자 유형입니다."), - UNEXPECTED_ERROR(443, "예상치 못한 오류가 발생했습니다."),; + UNEXPECTED_ERROR(443, "예상치 못한 오류가 발생했습니다."); private final int httpStatus; private final String message; From 6f56a646948edae0d41cac4b79a66183139ad5ea Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 15 Dec 2023 14:17:23 +0900 Subject: [PATCH 24/82] =?UTF-8?q?Chore:=20prometheus=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20dependency=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index df67569..b236475 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,12 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + + // prometheus grafana + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-core' + implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' From 81f92569bc1d2361b38ada85b9730cc6e42d9b5a Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 15 Dec 2023 20:55:19 +0900 Subject: [PATCH 25/82] =?UTF-8?q?Refactor:=20Dto=EB=A5=BC=20class=EC=97=90?= =?UTF-8?q?=EC=84=9C=20record=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/cart/CartService.java | 2 + .../domain/cart/dto/CartResponseDto.java | 33 +++---- .../domain/member/dto/LoginRequestDto.java | 5 +- .../domain/member/dto/SignupRequestDto.java | 5 +- .../domain/product/ProductRepository.java | 2 + .../product/dto/ProductListResponseDto.java | 21 +--- .../domain/product/dto/ProductRequestDto.java | 2 - .../product/dto/ProductResponseDto.java | 53 ++++------ .../domain/purchase/PurchaseController.java | 11 +-- .../domain/purchase/PurchaseService.java | 13 ++- .../purchase/dto/PurchaseRequestDto.java | 19 +--- .../purchase/dto/PurchaseResponseDto.java | 45 ++++----- .../domain/recipe/RecipeService.java | 4 +- .../domain/recipe/dto/RecipeRequestDto.java | 16 ++-- .../domain/recipe/dto/RecipeResponseDto.java | 38 ++++---- .../domain/recipe/entity/Recipe.java | 4 +- .../domain/purchase/PurchaseServiceTest.java | 96 ++++++++++++++----- 17 files changed, 178 insertions(+), 191 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index 5ca2073..80a27ed 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -39,12 +39,14 @@ public void addToCart(Long productId, CartRequestDto requestDto, Member member) @Transactional(readOnly = true) public List getCartList(Member member) { + new CartResponseDto(1L, "", 1, "NORMAL", "sdf", 1); return cartRepository.findAllByMember(member).stream() .map(cart -> { Product product = findAndValidateProduct(cart.getProduct().getId()); Image image = findImage(product.getId()); return CartResponseDto.of(product, image, cart); }).toList(); + } @Transactional diff --git a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java index 5c48eac..7288be8 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java @@ -5,31 +5,22 @@ import com.example.purebasketbe.domain.product.entity.Product; import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CartResponseDto { - - private Long id; - private String name; - private Integer price; - private String category; - private String imageUrl; - private int amount; +import lombok.RequiredArgsConstructor; +public record CartResponseDto( + Long id, + String name, + Integer price, + String category, + String imageUrl, + int amount +) { @Builder - private CartResponseDto(Long id, String name, Integer price, String category, String imageUrl, int amount) { - this.id = id; - this.name = name; - this.price = price; - this.category = category; - this.imageUrl = imageUrl; - this.amount = amount; + public CartResponseDto { + } - public static CartResponseDto of(Product product, Image image, Cart cart){ + public static CartResponseDto of(Product product, Image image, Cart cart) { return CartResponseDto.builder() .id(product.getId()) .name(product.getName()) diff --git a/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java b/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java index e62fa90..4b6b974 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/member/dto/LoginRequestDto.java @@ -3,11 +3,12 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public record LoginRequestDto ( +public record LoginRequestDto( @NotBlank(message = "이메일을 입력해주세요") @Email(message = "형식에 맞게 입력해주세요") String email, @NotBlank(message = "비밀번호를 입력해주세요") String password -) { } +) { +} diff --git a/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java b/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java index f0c1e3c..96e3df0 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/member/dto/SignupRequestDto.java @@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -public record SignupRequestDto ( +public record SignupRequestDto( @NotBlank(message = "이름을 입력해주세요") String name, @NotBlank(message = "이메일을 입력해주세요") @@ -19,4 +19,5 @@ public record SignupRequestDto ( @NotBlank(message = "전화번호를 입력해주세요") @Pattern(regexp = "^[0-9]{10,11}$", message = "전화번호는 10~11자리의 숫자이어야 합니다") String phone -){ } +) { +} diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index 93046ac..f66310e 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -25,4 +25,6 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet List findByIdIn(List requestIds); Optional findByIdAndDeleted(Long productId, boolean isDeleted); + + List findByIdInAndDeleted(List requestIds, boolean isDeleted); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java index 0ebd29d..1cc8145 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductListResponseDto.java @@ -2,29 +2,18 @@ import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import java.util.List; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProductListResponseDto { - - private Page eventProducts; - private Page products; +public record ProductListResponseDto(Page eventProducts, Page products) { @Builder - private ProductListResponseDto(Page eventProducts, - Page products) { - this.eventProducts = eventProducts; - this.products = products; + public ProductListResponseDto { + } - public static ProductListResponseDto of(Page eventProducts, - Page products) { + public static ProductListResponseDto of(Page eventProducts, Page products) { return ProductListResponseDto.builder() .eventProducts(eventProducts) .products(products) diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java index f43da61..ab85a32 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductRequestDto.java @@ -1,8 +1,6 @@ package com.example.purebasketbe.domain.product.dto; import com.example.purebasketbe.domain.product.entity.Event; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java index ff91d9d..740f0c8 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java @@ -5,40 +5,30 @@ import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import java.util.List; +import java.util.Objects; -@Getter -@JsonInclude(JsonInclude.Include.NON_NULL) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProductResponseDto { - private Long id; - private String name; - private int price; - private int stock; - private String info; - private String category; - private Event event; - private int discountRate; - private List images; +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ProductResponseDto( + Long id, + String name, + int price, + int stock, + String info, + String category, + Event event, + int discountRate, + List images +) { @Builder - ProductResponseDto(Long id, String name, int price, int stock, String info, - String category, Event event, int discountRate) { - this.id = id; - this.name = name; - this.price = price; - this.stock = stock; - this.info = info; - this.category = category; - this.event = event; - this.discountRate = discountRate; + public ProductResponseDto { } - public static ProductResponseDto from(Product product) { + public static ProductResponseDto of(Product product, List imgUrlList) { return ProductResponseDto.builder() .id(product.getId()) .name(product.getName()) @@ -48,16 +38,7 @@ public static ProductResponseDto from(Product product) { .category(product.getCategory()) .event(product.getEvent()) .discountRate(product.getDiscountRate()) + .images(imgUrlList) .build(); } - - public static ProductResponseDto of(Product product, List imgUrlLsit) { - ProductResponseDto productResponseDto = ProductResponseDto.from(product); - productResponseDto.addImgUrls(imgUrlLsit); - return productResponseDto; - } - - private void addImgUrls(List imgUrlList) { - this.images = imgUrlList; - } } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index be32f5a..a0db8d8 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -1,14 +1,13 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto; +import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.global.tool.LoginAccount; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -21,8 +20,8 @@ public class PurchaseController { @PostMapping public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseRequestDto requestDto, - @LoginAccount Member member) { - purchaseService.purchaseProducts(requestDto.getPurchaseList(), member); + @LoginAccount Member member) { + purchaseService.purchaseProducts(requestDto.purchaseList(), member); return ResponseEntity.status(HttpStatus.CREATED).build(); } @@ -30,8 +29,8 @@ public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseReq @GetMapping public ResponseEntity> getPurchases( - @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue="purchasedAt") String sortBy, - @RequestParam(defaultValue = "desc") String order, @LoginAccount Member member) { + @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "purchasedAt") String sortBy, + @RequestParam(defaultValue = "desc") String order, @LoginAccount Member member) { Page responseBody = purchaseService.getPurchases(member, page - 1, sortBy, order); return ResponseEntity.status(HttpStatus.OK).body(responseBody); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 373c6c6..6a6f0a6 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -4,9 +4,10 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.entity.Product; -import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; +import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; +import com.example.purebasketbe.domain.recipe.dto.RecipeResponseDto; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; @@ -35,15 +36,17 @@ public class PurchaseService { @Transactional public void purchaseProducts(final List purchaseRequestDto, Member member) { int size = purchaseRequestDto.size(); + List sortedPurchaseDetailList = purchaseRequestDto.stream() - .sorted(Comparator.comparing(PurchaseDetail::getProductId)).toList(); + .sorted(Comparator.comparing(PurchaseDetail::productId)).toList(); List requestIds = sortedPurchaseDetailList.stream() - .map(PurchaseDetail::getProductId).toList(); + .map(PurchaseDetail::productId).toList(); + - List validProductList = productRepository.findByIdIn(requestIds); + List validProductList = productRepository.findByIdInAndDeleted(requestIds, false); validateProducts(size, validProductList); List amountList = sortedPurchaseDetailList.stream() - .map(PurchaseDetail::getAmount).toList(); + .map(PurchaseDetail::amount).toList(); List purchaseList = new ArrayList<>(); for (int i = 0; i < size; i++) { diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java index f540822..f92cf43 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseRequestDto.java @@ -2,28 +2,13 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; import java.util.List; -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class PurchaseRequestDto { +public record PurchaseRequestDto(@NotNull List purchaseList) { - @NotNull - private List purchaseList; - - @Getter @Builder - public static class PurchaseDetail { - - @NotNull - private Long productId; - - @Min(value = 1, message = "1개 이상 입력하세요") - private int amount; + public record PurchaseDetail(@NotNull Long productId, @Min(value = 1, message = "1개 이상 입력하세요") int amount) { } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java index d048f94..80cdb09 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java @@ -2,38 +2,33 @@ import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.purchase.entity.Purchase; -import java.time.LocalDateTime; +import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; -@Getter -public class PurchaseResponseDto { - private final Long productId; - private final String name; - private final int amount; - private final int price; - private final int totalPrice; - private final LocalDateTime purchasedAt; +public record PurchaseResponseDto( + Long productId, + String name, + int amount, + int price, + int totalPrice, + LocalDateTime purchasedAt +) { @Builder - private PurchaseResponseDto(Long productId, String name, int amount, int price, int totalPrice, LocalDateTime purchasedAt) { - this.productId = productId; - this.name = name; - this.amount = amount; - this.price = price; - this.totalPrice = totalPrice; - this.purchasedAt = purchasedAt; + public PurchaseResponseDto { } public static PurchaseResponseDto of(Product product, Purchase purchase) { return PurchaseResponseDto.builder() - .productId(product.getId()) - .name(product.getName()) - .amount(purchase.getAmount()) - .price(purchase.getPrice()) - .totalPrice(purchase.getPrice() * purchase.getAmount()) - .purchasedAt(purchase.getPurchasedAt()) - .build(); + .productId(product.getId()) + .name(product.getName()) + .amount(purchase.getAmount()) + .price(purchase.getPrice()) + .totalPrice(purchase.getPrice() * purchase.getAmount()) + .purchasedAt(purchase.getPurchasedAt()) + .build(); } } - diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java index b7e0611..1774ce2 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java @@ -52,7 +52,7 @@ public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { String imgUrl = s3Handler.makeUrl(file); Recipe recipe = Recipe.from(requestDto, imgUrl); - for (Long productId : requestDto.getProductIdList()) { + for (Long productId : requestDto.productIdList()) { Product product = findValidProduct(productId); recipe.addProduct(product); } @@ -77,7 +77,7 @@ private Product findValidProduct(Long productId) { } private void checkExistRecipeByName(RecipeRequestDto requestDto) { - if (recipeRepository.existsByName(requestDto.getName())) { + if (recipeRepository.existsByName(requestDto.name())) { throw new CustomException(ErrorCode.RECIPE_ALREADY_EXISTS); } } diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeRequestDto.java b/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeRequestDto.java index 6639ea8..005f2d8 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeRequestDto.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeRequestDto.java @@ -1,21 +1,17 @@ package com.example.purebasketbe.domain.recipe.dto; import jakarta.validation.constraints.NotBlank; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; import java.util.List; -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class RecipeRequestDto { +public record RecipeRequestDto( - @NotBlank(message = "레시피명을 입력해 주세요.") - private String name; + @NotBlank(message = "레시피명을 입력해 주세요.") + String name, - private String info; + String info, - private List productIdList; + List productIdList +) { } diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeResponseDto.java b/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeResponseDto.java index dbcef1f..cd52232 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/dto/RecipeResponseDto.java @@ -4,28 +4,24 @@ import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.recipe.entity.Recipe; import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AccessLevel; import lombok.Builder; -import lombok.Getter; +import lombok.RequiredArgsConstructor; import org.springframework.util.StringUtils; import java.util.List; -@Getter @JsonInclude(JsonInclude.Include.NON_NULL) -public class RecipeResponseDto { - private final Long id; - private final String name; - private final String info; - private final String imgUrl; - private final List products; +public record RecipeResponseDto( + Long id, + String name, + String info, + String imgUrl, + List products +) { @Builder - private RecipeResponseDto(Long id, String name, String info, String imgUrl, List products) { - this.id = id; - this.name = name; - this.info = info; - this.imgUrl = imgUrl; - this.products = products; + public RecipeResponseDto { } public static RecipeResponseDto from(Recipe recipe) { @@ -52,14 +48,14 @@ public static RecipeResponseDto of(Recipe recipe, List productList) { } - @Getter @Builder - private static class RelatedProductResponseDto { - private Long id; - private String name; - private Integer price; - private Event event; - private String imgUrl; + private record RelatedProductResponseDto( + Long id, + String name, + Integer price, + Event event, + String imgUrl + ) { private static RelatedProductResponseDto from(Product product) { String imgUrl = product.getImages().isEmpty() ? "default Url" : product.getImages().get(0).getImgUrl(); diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java b/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java index 7426a87..a61a227 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/entity/Recipe.java @@ -47,8 +47,8 @@ private Recipe(String name, String info, String imgUrl, List productLis public static Recipe from(RecipeRequestDto requestDto, String imgUrl) { return Recipe.builder() - .name(requestDto.getName()) - .info(requestDto.getInfo()) + .name(requestDto.name()) + .info(requestDto.info()) .imgUrl(imgUrl) .productList(new ArrayList<>()) .build(); diff --git a/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java b/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java index 0410a1c..cefef1e 100644 --- a/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java @@ -2,8 +2,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import com.example.purebasketbe.domain.cart.CartRepository; import com.example.purebasketbe.domain.member.entity.Member; @@ -14,9 +16,12 @@ import com.example.purebasketbe.domain.purchase.entity.Purchase; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; + import java.util.ArrayList; import java.util.List; import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -25,6 +30,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -50,30 +56,72 @@ class PurchaseServiceTest { @DisplayName("주문") class PurchaseProducts { + private Member member; + private List purchaseRequestDto; + + @BeforeEach + void setUp() { + member = Member.builder().build(); + purchaseRequestDto = List.of( + PurchaseDetail.builder().productId(1L).amount(2).build(), + PurchaseDetail.builder().productId(2L).amount(3).build() + ); + } + @Test @DisplayName("주문 성공") void purchaseProductsSuccess() { // given - Product product1 = Product.builder().build(); - Product product2 = Product.builder().build(); - PurchaseDetail purchaseDetail1 = PurchaseDetail.builder().productId(1L).amount(2).build(); - PurchaseDetail purchaseDetail2 = PurchaseDetail.builder().productId(2L).amount(3).build(); - List purchaseRequestDto = List.of(purchaseDetail1, purchaseDetail2); + List validProductList = prepareValidProductList(); + given(productRepository.findByIdInAndDeleted(any(), eq(false))).willReturn(validProductList); + + // when + purchaseService.purchaseProducts(purchaseRequestDto, member); + + // then + verify(purchaseRepository, times(1)).saveAll(any()); + verify(cartRepository, times(1)).deleteByUserAndProductIn(member, validProductList); -// given(productRepository.findByIdIn(any())).willReturn() + } + + @Test + @DisplayName("주문 실패 - 존재하지 않는 상품") + void purchaseProductsFail1() { + // given // when + Exception exception = assertThrows(CustomException.class, () -> + purchaseService.purchaseProducts(purchaseRequestDto, member) + ); // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); } + @Test - @DisplayName("주문 실패") - void purchaseProductsFail() { + @DisplayName("주문 실패 - 재고 부족") + void purchaseProductsFail2() { // given + Product product1 = Product.builder().stock(1).price(1000).discountRate(0).build(); + Product product2 = Product.builder().stock(1).price(1000).discountRate(0).build(); + List validProductList = List.of(product1, product2); + + given(productRepository.findByIdInAndDeleted(any(), eq(false))).willReturn(validProductList); // when + Exception exception = assertThrows(CustomException.class, () -> + purchaseService.purchaseProducts(purchaseRequestDto, member) + ); // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.NOT_ENOUGH_PRODUCT.getMessage()); + } + + private List prepareValidProductList() { + return List.of( + Product.builder().stock(4).price(1000).discountRate(0).build(), + Product.builder().stock(5).price(10000).discountRate(0).build() + ); } } @@ -82,7 +130,7 @@ void purchaseProductsFail() { class GetPurchaseHistory { @DisplayName("주문 내역 조회 성공") @ParameterizedTest - @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter=':') + @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter = ':') void getPurchaseHistorySuccess(String order, String sortBy) { // given int PRODUCTS_PER_PAGE = 20; @@ -93,22 +141,22 @@ void getPurchaseHistorySuccess(String order, String sortBy) { Sort sort = Sort.by(direction, sortBy); Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, sort); - Product product = Product.builder().build(); + Product product = Product.builder().stock(4).price(1000).discountRate(0).build(); List purchaseList = new ArrayList<>(); for (int i = 0; i < PRODUCTS_PER_PAGE * 2; i++) { Purchase purchase = Purchase.builder() - .member(member) - .product(product) - .build(); + .member(member) + .product(product) + .build(); purchaseList.add(purchase); } int start = (int) pageRequest.getOffset(); int end = Math.min((start + pageRequest.getPageSize()), purchaseList.size()); Page purchases = new PageImpl<>(purchaseList.subList(start, end), pageRequest, - purchaseList.size()); + purchaseList.size()); - given(purchaseRepository.findAllByMember(member, pageRequest)).willReturn(purchases); + given(purchaseRepository.findAllByMember(any(), any())).willReturn(purchases); given(productRepository.findById(any())).willReturn(Optional.of(product)); // when @@ -121,7 +169,7 @@ void getPurchaseHistorySuccess(String order, String sortBy) { @DisplayName("주문 내역 조회 실패") @ParameterizedTest - @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter=':') + @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter = ':') void getPurchaseHistoryFail(String order, String sortBy) { // given int PRODUCTS_PER_PAGE = 20; @@ -132,23 +180,23 @@ void getPurchaseHistoryFail(String order, String sortBy) { Sort sort = Sort.by(direction, sortBy); Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, sort); - Product product = Product.builder().build(); + Product product = Product.builder().stock(4).price(1000).discountRate(0).build(); List purchaseList = new ArrayList<>(); Purchase purchase = Purchase.builder() - .member(member) - .product(product) - .build(); + .member(member) + .product(product) + .build(); purchaseList.add(purchase); Page purchases = new PageImpl<>(purchaseList, pageRequest, - purchaseList.size()); + purchaseList.size()); - given(purchaseRepository.findAllByMember(member, pageRequest)).willReturn(purchases); + given(purchaseRepository.findAllByMember(any(), any())).willReturn(purchases); given(productRepository.findById(any())).willReturn(Optional.empty()); // when Exception exception = assertThrows(CustomException.class, - () -> purchaseService.getPurchases(member, page, sortBy, order)); + () -> purchaseService.getPurchases(member, page, sortBy, order)); // then assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); From 7944b24450505fe4a2207f88475d58dfa5c3dced Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 11:23:25 +0900 Subject: [PATCH 26/82] =?UTF-8?q?Refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/purebasketbe/domain/cart/CartService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index 80a27ed..ef4f01f 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -39,7 +39,6 @@ public void addToCart(Long productId, CartRequestDto requestDto, Member member) @Transactional(readOnly = true) public List getCartList(Member member) { - new CartResponseDto(1L, "", 1, "NORMAL", "sdf", 1); return cartRepository.findAllByMember(member).stream() .map(cart -> { Product product = findAndValidateProduct(cart.getProduct().getId()); From d6209e907d59df391c946eb19b730bdd9471b30a Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 11:24:00 +0900 Subject: [PATCH 27/82] =?UTF-8?q?Refactor:=20RecipeService=EC=97=90=20getR?= =?UTF-8?q?ecipeById=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/recipe/RecipeService.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java index 1774ce2..d66a1b4 100644 --- a/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java +++ b/src/main/java/com/example/purebasketbe/domain/recipe/RecipeService.java @@ -39,8 +39,7 @@ public Page getRecipes(int page) { @Transactional(readOnly = true) public RecipeResponseDto getRecipe(Long recipeId) { - Recipe recipe = recipeRepository.findById(recipeId).orElseThrow( - () -> new CustomException(ErrorCode.RECIPE_NOT_FOUND)); + Recipe recipe = getRecipeById(recipeId); List productList = recipe.getProductList(); return RecipeResponseDto.of(recipe, productList); @@ -63,13 +62,16 @@ public void registerRecipe(RecipeRequestDto requestDto, MultipartFile file) { @Transactional public void deleteRecipe(Long recipeId) { - Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(() -> - new CustomException(ErrorCode.RECIPE_NOT_FOUND) - ); + Recipe recipe = getRecipeById(recipeId); s3Handler.deleteImage(recipe.getImgUrl()); recipeRepository.delete(recipe); } + private Recipe getRecipeById(Long recipeId) { + return recipeRepository.findById(recipeId).orElseThrow( + () -> new CustomException(ErrorCode.RECIPE_NOT_FOUND)); + } + private Product findValidProduct(Long productId) { return productRepository.findByIdAndDeleted(productId, false).orElseThrow(() -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) From 7431096829aba91a4ce31915b1a58e16ad9bfa97 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 11:25:49 +0900 Subject: [PATCH 28/82] =?UTF-8?q?Test:=20PurchaseServiceTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/purchase/PurchaseServiceTest.java | 123 ++++++++---------- 1 file changed, 55 insertions(+), 68 deletions(-) diff --git a/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java b/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java index cefef1e..a6620e4 100644 --- a/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/purchase/PurchaseServiceTest.java @@ -1,26 +1,14 @@ package com.example.purebasketbe.domain.purchase; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import com.example.purebasketbe.domain.cart.CartRepository; import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.entity.Product; -import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; +import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -30,15 +18,21 @@ import org.junit.jupiter.params.provider.CsvSource; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.data.domain.Sort.Direction; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + @ExtendWith(MockitoExtension.class) class PurchaseServiceTest { @@ -56,8 +50,8 @@ class PurchaseServiceTest { @DisplayName("주문") class PurchaseProducts { - private Member member; private List purchaseRequestDto; + private Member member; @BeforeEach void setUp() { @@ -73,15 +67,14 @@ void setUp() { void purchaseProductsSuccess() { // given List validProductList = prepareValidProductList(); - given(productRepository.findByIdInAndDeleted(any(), eq(false))).willReturn(validProductList); + given(productRepository.findByIdInAndDeleted(anyList(), eq(false))).willReturn(validProductList); // when purchaseService.purchaseProducts(purchaseRequestDto, member); // then - verify(purchaseRepository, times(1)).saveAll(any()); + verify(purchaseRepository, times(1)).saveAll(anyList()); verify(cartRepository, times(1)).deleteByUserAndProductIn(member, validProductList); - } @Test @@ -106,7 +99,7 @@ void purchaseProductsFail2() { Product product2 = Product.builder().stock(1).price(1000).discountRate(0).build(); List validProductList = List.of(product1, product2); - given(productRepository.findByIdInAndDeleted(any(), eq(false))).willReturn(validProductList); + given(productRepository.findByIdInAndDeleted(anyList(), eq(false))).willReturn(validProductList); // when Exception exception = assertThrows(CustomException.class, () -> @@ -128,43 +121,39 @@ private List prepareValidProductList() { @Nested @DisplayName("주문 내역 조회") class GetPurchaseHistory { + private static final int PRODUCTS_PER_PAGE = 20; + private static final int page = 0; + private Member member; + private Product product; + private Purchase purchase; + + @BeforeEach + void setUp() { + member = Member.builder().build(); + product = Product.builder().stock(4).price(1000).discountRate(0).build(); + purchase = Purchase.builder() + .member(member) + .product(product) + .build(); + } + @DisplayName("주문 내역 조회 성공") @ParameterizedTest @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter = ':') void getPurchaseHistorySuccess(String order, String sortBy) { // given - int PRODUCTS_PER_PAGE = 20; - int page = 0; - - Member member = Member.builder().build(); - Sort.Direction direction = Direction.valueOf(order.toUpperCase()); - Sort sort = Sort.by(direction, sortBy); - Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, sort); - - Product product = Product.builder().stock(4).price(1000).discountRate(0).build(); - List purchaseList = new ArrayList<>(); - for (int i = 0; i < PRODUCTS_PER_PAGE * 2; i++) { - Purchase purchase = Purchase.builder() - .member(member) - .product(product) - .build(); - purchaseList.add(purchase); - } + Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, createSort(order, sortBy)); + Page purchases = createPurchasePage(pageRequest); - int start = (int) pageRequest.getOffset(); - int end = Math.min((start + pageRequest.getPageSize()), purchaseList.size()); - Page purchases = new PageImpl<>(purchaseList.subList(start, end), pageRequest, - purchaseList.size()); - - given(purchaseRepository.findAllByMember(any(), any())).willReturn(purchases); + given(purchaseRepository.findAllByMember(any(Member.class), any(Pageable.class))).willReturn(purchases); given(productRepository.findById(any())).willReturn(Optional.of(product)); // when Page result = purchaseService.getPurchases(member, page, sortBy, order); // then - assertThat(result.getTotalPages()).isEqualTo(2); - assertThat(result.getTotalElements()).isEqualTo(PRODUCTS_PER_PAGE * 2); + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getTotalPages()).isEqualTo(1); } @DisplayName("주문 내역 조회 실패") @@ -172,26 +161,10 @@ void getPurchaseHistorySuccess(String order, String sortBy) { @CsvSource(value = {"desc:purchasedAt", "asc:purchasedAt", "desc:name", "asc:name"}, delimiter = ':') void getPurchaseHistoryFail(String order, String sortBy) { // given - int PRODUCTS_PER_PAGE = 20; - int page = 0; + Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, createSort(order, sortBy)); + Page purchases = createPurchasePage(pageRequest); - Member member = Member.builder().build(); - Sort.Direction direction = Direction.valueOf(order.toUpperCase()); - Sort sort = Sort.by(direction, sortBy); - Pageable pageRequest = PageRequest.of(page, PRODUCTS_PER_PAGE, sort); - - Product product = Product.builder().stock(4).price(1000).discountRate(0).build(); - List purchaseList = new ArrayList<>(); - Purchase purchase = Purchase.builder() - .member(member) - .product(product) - .build(); - purchaseList.add(purchase); - - Page purchases = new PageImpl<>(purchaseList, pageRequest, - purchaseList.size()); - - given(purchaseRepository.findAllByMember(any(), any())).willReturn(purchases); + given(purchaseRepository.findAllByMember(any(Member.class), any(Pageable.class))).willReturn(purchases); given(productRepository.findById(any())).willReturn(Optional.empty()); // when @@ -202,5 +175,19 @@ void getPurchaseHistoryFail(String order, String sortBy) { assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); } + private Sort createSort(String order, String sortBy) { + Sort.Direction direction = Direction.valueOf(order.toUpperCase()); + return Sort.by(direction, sortBy); + } + + private Page createPurchasePage(Pageable pageRequest) { + List purchaseList = new ArrayList<>(); + purchaseList.add(purchase); + + int start = (int) pageRequest.getOffset(); + int end = Math.min((start + pageRequest.getPageSize()), purchaseList.size()); + return new PageImpl<>(purchaseList.subList(start, end), pageRequest, + purchaseList.size()); + } } } \ No newline at end of file From 190a3763d6724bdf08946ca516da4587ac366487 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 11:26:07 +0900 Subject: [PATCH 29/82] =?UTF-8?q?Test:=20RecipeServiceTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/recipe/RecipeServiceTest.java | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java diff --git a/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java b/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java new file mode 100644 index 0000000..fd627c5 --- /dev/null +++ b/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java @@ -0,0 +1,218 @@ +package com.example.purebasketbe.domain.recipe; + +import com.example.purebasketbe.domain.product.ProductRepository; +import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.recipe.dto.RecipeRequestDto; +import com.example.purebasketbe.domain.recipe.dto.RecipeResponseDto; +import com.example.purebasketbe.domain.recipe.entity.Recipe; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.s3.S3Handler; +import org.junit.jupiter.api.BeforeEach; +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.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +class RecipeServiceTest { + @Mock + RecipeRepository recipeRepository; + + @Mock + ProductRepository productRepository; + @Mock + S3Handler s3Handler; + + @InjectMocks + RecipeService recipeService; + + @Test + @DisplayName("레시피 목록 조회") + void getRecipes() { + // given + int page = 0; + final int RECIPES_PER_PAGE = 10; + Pageable pageRequest = PageRequest.of(page, RECIPES_PER_PAGE); + List recipeList = new ArrayList<>(); + Recipe recipe = Recipe.builder().build(); + recipeList.add(recipe); + Page recipes = new PageImpl<>(recipeList, pageRequest, + recipeList.size()); +// Page recipes = mock(Page.class); + + given(recipeRepository.findAll(pageRequest)).willReturn(recipes); +// given(recipeRepository.findAll(any(Pageable.class)).willReturn(recipes); + + // when + Page result = recipeService.getRecipes(page); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getTotalPages()).isEqualTo(1); + verify(recipeRepository).findAll(any(Pageable.class)); + } + + @Nested + @DisplayName("레시피 상세 조회") + class getRecipe { + @Test + @DisplayName("레시피 상세 조회 성공") + void getRecipeSuccess() { + // given + Long recipeId = 1L; + List productList = new ArrayList<>(); + Recipe recipe = Recipe.builder().name("test recipe").productList(productList).build(); + given(recipeRepository.findById(any())).willReturn(Optional.of(recipe)); + + // when + RecipeResponseDto result = recipeService.getRecipe(recipeId); + + // then + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo("test recipe"); + verify(recipeRepository).findById(recipeId); + } + + @Test + @DisplayName("레시피 상세 조회 실패") + void getRecipeFail() { + // given + Long recipeId = 1L; + given(recipeRepository.findById(any())).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> recipeService.getRecipe(recipeId) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.RECIPE_NOT_FOUND.getMessage()); + } + + + } + + @Nested + @DisplayName("레시피 등록") + class RegisterRecipe { + + private MultipartFile file; + + private RecipeRequestDto requestDto; + + @BeforeEach + void setUp() { + file = mock(MultipartFile.class); + List productIdList = new ArrayList<>(List.of(1L, 2L)); + requestDto = new RecipeRequestDto("test recipe", "info", productIdList); + } + + @Test + @DisplayName("레시피 등록 성공") + void registerRecipeSuccess() { + // given + Product product = Product.builder().price(1000).stock(10).discountRate(0).build(); + given(recipeRepository.existsByName(requestDto.name())).willReturn(false); + given(s3Handler.makeUrl(file)).willReturn("mockUrl"); + given(productRepository.findByIdAndDeleted(any(), eq(false))).willReturn(Optional.of(product)); + + // when + recipeService.registerRecipe(requestDto, file); + + // then + verify(s3Handler, times(1)).makeUrl(file); + verify(recipeRepository, times(1)).existsByName(requestDto.name()); + verify(recipeRepository, times(1)).save(any(Recipe.class)); + verify(s3Handler, times(1)).uploadImages("mockUrl", file); + } + + @Test + @DisplayName("레시피 등록 실패 - 이미 존재하는 레시피") + void registerRecipeFail1() { + // given + given(recipeRepository.existsByName(requestDto.name())).willReturn(true); + + // when + Exception exception = assertThrows(CustomException.class, + () -> recipeService.registerRecipe(requestDto, file) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.RECIPE_ALREADY_EXISTS.getMessage()); + } + + @Test + @DisplayName("레시피 등록 실패 - 존재하지 않는 관련 상품") + void registerRecipeFail2() { + // given + given(recipeRepository.existsByName(requestDto.name())).willReturn(false); + given(productRepository.findByIdAndDeleted(any(), eq(false))).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> recipeService.registerRecipe(requestDto, file) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + } + + @Nested + @DisplayName("레시피 삭제") + class DeleteRecipe { + @Test + @DisplayName("레시피 삭제 성공") + void deleteRecipeSuccess() { + // given + Long recipeId = 1L; + Recipe recipe = Recipe.builder().build(); + given(recipeRepository.findById(any())).willReturn(Optional.of(recipe)); + + // when + recipeService.deleteRecipe(recipeId); + + // then + verify(s3Handler, times(1)).deleteImage(any()); + verify(recipeRepository, times(1)).delete(recipe); + } + + @Test + @DisplayName("레시피 삭제 실패") + void deleteRecipeFail() { + // given + Long recipeId = 1L; + given(recipeRepository.findById(any())).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> recipeService.deleteRecipe(recipeId) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.RECIPE_NOT_FOUND.getMessage()); + } + } +} \ No newline at end of file From 1b2fdb06977f5c70dda8391aa4a06d19ed9ef4b4 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 13:08:07 +0900 Subject: [PATCH 30/82] =?UTF-8?q?Update:=20Security=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EC=99=B8=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/purebasketbe/PureBasketBeApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java index 432e69a..9fb75b3 100644 --- a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java +++ b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing -@SpringBootApplication(exclude = SecurityAutoConfiguration.class) // Spring Security 인증 기능 제외 +@SpringBootApplication public class PureBasketBeApplication { public static void main(String[] args) { From 162083b147d48016312a3d1c843039b44c2431b5 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 19:38:53 +0900 Subject: [PATCH 31/82] =?UTF-8?q?Update:=20Cart=20domain=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cart entity에 of 메서드 오버라이딩 CartService에서 checkDuplicate 메서드명을 checkIfExist로 변경 getRecipeById 메서드 추출해서 생성 findAndValidateProduct 메서드 로직 수정 --- .../purebasketbe/domain/cart/CartService.java | 24 +++++++++---------- .../purebasketbe/domain/cart/entity/Cart.java | 11 +++++++-- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index ef4f01f..48f2417 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -32,7 +32,7 @@ public class CartService { @Transactional public void addToCart(Long productId, CartRequestDto requestDto, Member member) { Product product = findAndValidateProduct(productId); - chcekDuplicate(product); + checkIfExist(product); Cart newCart = Cart.of(product, member, requestDto); cartRepository.save(newCart); } @@ -45,7 +45,6 @@ public List getCartList(Member member) { Image image = findImage(product.getId()); return CartResponseDto.of(product, image, cart); }).toList(); - } @Transactional @@ -62,27 +61,22 @@ public void deleteCart(Long productId, Member member) { @Transactional public void addRecipeRelatedProductsToCarts(Long recipeId, Member member) { - Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(() -> - new CustomException(ErrorCode.RECIPE_NOT_FOUND) - ); + Recipe recipe = getRecipeById(recipeId); List productList = recipe.getProductList(); cartRepository.deleteAllByMemberAndProductIn(member, productList); - List cartList = productList.stream().map(product -> Cart.of(product, member, null)).toList(); + List cartList = productList.stream().map(product -> Cart.of(product, member)).toList(); cartRepository.saveAll(cartList); } + private Product findAndValidateProduct(Long productId) { - Product product = productRepository.findById(productId).orElseThrow( + return productRepository.findByIdAndDeleted(productId, false).orElseThrow( () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) ); - if (product.getStock() <= 0 || product.isDeleted()) { - throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); - } - return product; } - private void chcekDuplicate(Product product) { + private void checkIfExist(Product product) { if (cartRepository.existsProduct(product)) { throw new CustomException(ErrorCode.PRODUCT_ALREADY_ADDED); } @@ -98,4 +92,10 @@ private Image findImage(Long productId) { return imageRepository.findAllByProductId(productId).stream().findFirst() .orElseThrow(() -> new CustomException(ErrorCode.INVALID_IMAGE)); } + + private Recipe getRecipeById(Long recipeId) { + return recipeRepository.findById(recipeId).orElseThrow(() -> + new CustomException(ErrorCode.RECIPE_NOT_FOUND) + ); + } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/cart/entity/Cart.java b/src/main/java/com/example/purebasketbe/domain/cart/entity/Cart.java index fd90fd2..8f856ea 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/entity/Cart.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/entity/Cart.java @@ -38,10 +38,17 @@ private Cart(int amount, Member member, Product product) { this.product = product; } + public static Cart of(Product product, Member member) { + return Cart.builder() + .amount(1) + .member(member) + .product(product) + .build(); + } + public static Cart of(Product product, Member member, CartRequestDto requestDto) { - int amount = requestDto == null ? 1 : requestDto.amount(); return Cart.builder() - .amount(amount) + .amount(requestDto.amount()) .member(member) .product(product) .build(); From 767553ceb3f2d227f1090d7d66ffe49a2c80db4f Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 19:39:37 +0900 Subject: [PATCH 32/82] =?UTF-8?q?Test:=20CartServiceTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/cart/CartServiceTest.java | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java diff --git a/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java b/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java new file mode 100644 index 0000000..c7d0310 --- /dev/null +++ b/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java @@ -0,0 +1,281 @@ +package com.example.purebasketbe.domain.cart; + +import com.example.purebasketbe.domain.cart.dto.CartRequestDto; +import com.example.purebasketbe.domain.cart.dto.CartResponseDto; +import com.example.purebasketbe.domain.cart.entity.Cart; +import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.domain.product.ImageRepository; +import com.example.purebasketbe.domain.product.ProductRepository; +import com.example.purebasketbe.domain.product.entity.Image; +import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.recipe.RecipeRepository; +import com.example.purebasketbe.domain.recipe.entity.Recipe; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import org.junit.jupiter.api.BeforeEach; +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 java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class CartServiceTest { + @Mock + CartRepository cartRepository; + + @Mock + ProductRepository productRepository; + + @Mock + RecipeRepository recipeRepository; + + @Mock + ImageRepository imageRepository; + + @InjectMocks + CartService cartService; + + + private Long productId; + private Member member; + private CartRequestDto requestDto; + private Product product; + + @BeforeEach + void setUp() { + productId = 1L; + member = Member.builder().build(); + requestDto = new CartRequestDto(1); + product = Product.builder().price(10000).stock(10).discountRate(0).build(); + } + + @Nested + @DisplayName("장바구니 담기") + class AddToCart { + + @Test + @DisplayName("장바구니 담기 성공") + void addToCartSuccess() { + // given + given(productRepository.findByIdAndDeleted(productId, false)).willReturn(Optional.of(product)); + given(cartRepository.existsProduct(product)).willReturn(false); + + // when + cartService.addToCart(productId, requestDto, member); + + // then + verify(cartRepository).save(any(Cart.class)); + } + + @Test + @DisplayName("장바구니 담기 실패 - 존재하지 않는 상품") + void addToCartFail1() { + // given + given(productRepository.findByIdAndDeleted(productId, false)).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.addToCart(productId, requestDto, member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("장바구니 담기 실패 - 이미 등록된 상품") + void addToCartFail2() { + // given + + given(productRepository.findByIdAndDeleted(productId, false)).willReturn(Optional.of(product)); + given(cartRepository.existsProduct(product)).willReturn(true); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.addToCart(productId, requestDto, member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_ALREADY_ADDED.getMessage()); + + } + } + + @Nested + @DisplayName("장바구니 리스트 조회") + class GetCartList { + @Test + @DisplayName("장바구니 리스트 조회 성공") + void getCartListSuccess() { + // given + List imageList = List.of(mock(Image.class), mock(Image.class)); + List cartList = List.of(Cart.builder().product(product).build()); + given(cartRepository.findAllByMember(member)).willReturn(cartList); + given(productRepository.findByIdAndDeleted(any(), eq(false))).willReturn(Optional.of(product)); + given(imageRepository.findAllByProductId(any())).willReturn(imageList); + + // when + List result = cartService.getCartList(member); + + // then + assertThat(result.size()).isEqualTo(1); + } + + @Test + @DisplayName("장바구니 리스트 조회 실패 - 존재하지 않은 상품") + void getCartListFail1() { + // given + List cartList = List.of(Cart.builder().product(product).build()); + given(cartRepository.findAllByMember(member)).willReturn(cartList); + given(productRepository.findByIdAndDeleted(any(), eq(false))).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.getCartList(member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("장바구니 리스트 조회 실패 - 유효하지 않은 이미지") + void getCartListFail2() { + // given + List cartList = List.of(Cart.builder().product(product).build()); + given(cartRepository.findAllByMember(member)).willReturn(cartList); + given(productRepository.findByIdAndDeleted(any(), eq(false))).willReturn(Optional.of(product)); + given(imageRepository.findAllByProductId(any())).willReturn(new ArrayList<>()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.getCartList(member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.INVALID_IMAGE.getMessage()); + } + } + + @Nested + @DisplayName("장바구니 업데이트") + class UpdateCart { + + @Test + @DisplayName("장바구니 업데이트 성공") + void updateCartSuccess() { + // given + Cart cart = mock(Cart.class); + given(cartRepository.findByProductIdAndMember(productId, member)).willReturn(Optional.of(cart)); + + // when + cartService.updateCart(productId, requestDto, member); + + // then + verify(cart).changeAmount(requestDto); + } + + @Test + @DisplayName("장바구니 업데이트 실패") + void updateCartFail() { + // given + given(cartRepository.findByProductIdAndMember(productId, member)).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.updateCart(productId, requestDto, member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.INVALID_CART_ITEM.getMessage()); + } + } + + @Nested + @DisplayName("장바구니 삭제") + class DeleteCart { + + @Test + @DisplayName("장바구니 삭제 성공") + void deleteCartSuccess() { + // given + Cart cart = mock(Cart.class); + given(cartRepository.findByProductIdAndMember(productId, member)).willReturn(Optional.of(cart)); + + // when + cartService.deleteCart(productId, member); + + // then + verify(cartRepository).delete(cart); + } + + @Test + @DisplayName("장바구니 삭제 실패") + void deleteCartFail() { + // given + given(cartRepository.findByProductIdAndMember(productId, member)).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.deleteCart(productId, member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.INVALID_CART_ITEM.getMessage()); + } + } + + @Nested + @DisplayName("레시피 관련 상품 장바구니 추가") + class AddRecipeRelatedProductsToCart { + @Test + @DisplayName("레시피 관련 상품 장바구니 추가 성공") + void addRecipeRelatedProductsToCartSuccess() { + // given + Long recipeId = 1L; + List productList = List.of(product); + Recipe recipe = Recipe.builder() + .productList(productList) + .build(); + + given(recipeRepository.findById(recipeId)).willReturn(Optional.of(recipe)); + + // when + cartService.addRecipeRelatedProductsToCarts(recipeId, member); + + // then + verify(cartRepository).deleteAllByMemberAndProductIn(member, productList); + verify(cartRepository).saveAll(anyList()); + } + + @Test + @DisplayName("레시피 관련 상품 장바구니 추가 실패") + void addRecipeRelatedProductsToCartFail() { + // given + Long recipeId = 1L; + given(recipeRepository.findById(recipeId)).willReturn(Optional.empty()); + + // when + Exception exception = assertThrows(CustomException.class, + () -> cartService.addRecipeRelatedProductsToCarts(recipeId, member) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.RECIPE_NOT_FOUND.getMessage()); + } + } + +} \ No newline at end of file From 2ff02a8470227e63222513a80d1a80ad6c7bd07d Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 20:53:27 +0900 Subject: [PATCH 33/82] =?UTF-8?q?Test:=20MemberServiceTest,=20AuthServiceT?= =?UTF-8?q?est=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/admin/AuthServiceTest.java | 64 +++++++++ .../domain/member/MemberServiceTest.java | 121 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java create mode 100644 src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java diff --git a/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java b/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java new file mode 100644 index 0000000..f8e9ca2 --- /dev/null +++ b/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java @@ -0,0 +1,64 @@ +package com.example.purebasketbe.domain.admin; + +import com.example.purebasketbe.domain.member.dto.LoginRequestDto; +import org.junit.jupiter.api.BeforeEach; +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.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + @Mock + AuthenticationManager authenticationManager; + + @InjectMocks + AuthService authService; + + + private LoginRequestDto requestDto; + @BeforeEach + void setUp() { + requestDto = new LoginRequestDto("test@gmail.com", "password"); + } + + @Test + @DisplayName("관리자 로그인 성공") + void authenticationAdminSuccess() { + // given + Authentication authentication = mock(Authentication.class); + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password()); + given(authenticationManager.authenticate(authenticationToken)).willReturn(authentication); + + // when + Authentication result = authService.authenticateAdmin(requestDto); + + // then + assertThat(result).isNotNull(); + + } + @Test + @DisplayName("관리자 로그인 실패") + void authenticationAdminFail() { + // given + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(requestDto.email(), requestDto.password()); + given(authenticationManager.authenticate(authenticationToken)).willThrow(RuntimeException.class); + + // when - then + assertThrows(RuntimeException.class, + () -> authService.authenticateAdmin(requestDto) + ); + } +} diff --git a/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java b/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java new file mode 100644 index 0000000..2e0d057 --- /dev/null +++ b/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java @@ -0,0 +1,121 @@ +package com.example.purebasketbe.domain.member; + +import com.example.purebasketbe.domain.member.dto.SignupRequestDto; +import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import org.junit.jupiter.api.BeforeEach; +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.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest { + @Mock + MemberRepository memberRepository; + + @Mock + PasswordEncoder passwordEncoder; + + @InjectMocks + MemberService memberService; + + @Nested + @DisplayName("회원가입") + class Register { + + + private SignupRequestDto requestDto; + + @BeforeEach + void setUp() { + requestDto = new SignupRequestDto( + "test name", + "test mail", + "testpassword", + "test address", + "01012345678" + ); + } + + @Test + @DisplayName("회원가입 성공") + + void RegisterSuccess() { + // given + given(passwordEncoder.encode(requestDto.password())).willReturn("encoded"); + given(memberRepository.existsByEmail(requestDto.email())).willReturn(false); + + // when + memberService.registerMember(requestDto); + + // then + verify(memberRepository).save(any(Member.class)); + } + + @Test + @DisplayName("회원가입 실패 - 이미 존재하는 이메일") + void RegisterFail() { + // given + given(passwordEncoder.encode(requestDto.password())).willReturn("encoded"); + given(memberRepository.existsByEmail(requestDto.email())).willReturn(true); + + // when + Exception exception = assertThrows(CustomException.class, + () -> memberService.registerMember(requestDto) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.EMAIL_ALREADY_EXISTS.getMessage()); + } + } + + @Nested + @DisplayName("이메일 확인") + class FindEmail { + @Test + @DisplayName("이메일 확인 성공") + void findMemberByEmailSuccess() { + // given + String email = "test@gmail.com"; + Member member = Member.builder().email(email).build(); + given(memberRepository.findByEmail(email)).willReturn(Optional.of(member)); + + // when + Member result = memberService.findMemberByEmail(email); + + // then + assertThat(result.getEmail()).isEqualTo(email); + } + @Test + @DisplayName("이메일 확인 실패") + void findMemberByEmailFail() { + // given + String email = "test@gmail.com"; + + // when + Exception exception = assertThrows(CustomException.class, + () -> memberService.findMemberByEmail(email) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.EMAIL_NOT_FOUND.getMessage()); + } + } + +} \ No newline at end of file From 6d676bce93874f01112634c1f1bcad3301bf6e8d Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 22:47:28 +0900 Subject: [PATCH 34/82] =?UTF-8?q?Test:=20s3HandlerTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/global/s3/S3HandlerTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java diff --git a/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java b/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java new file mode 100644 index 0000000..fbe4ba9 --- /dev/null +++ b/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java @@ -0,0 +1,126 @@ +package com.example.purebasketbe.global.s3; + +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import io.awspring.cloud.s3.ObjectMetadata; +import io.awspring.cloud.s3.S3Template; +import org.junit.jupiter.api.BeforeEach; +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.junit.jupiter.MockitoExtension; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class S3HandlerTest { + @Mock + S3Template s3Template; + + @InjectMocks + S3Handler s3Handler; + + + @Test + @DisplayName("파일에서 url 생성") + void makeUrl() { + // given + MultipartFile file = mock(MultipartFile.class); + + // when + String result = s3Handler.makeUrl(file); + + // then + assertThat(result).isNotNull(); + } + + @Test + @DisplayName("이미지 업로드 성공") + void uploadImageSuccess() throws IOException { + // given + String imgUrl = "imgUrl/key"; + MultipartFile file = mock(MultipartFile.class); + InputStream inputStream = mock(InputStream.class); + + given(file.getInputStream()).willReturn(inputStream); + + // when + s3Handler.uploadImages(imgUrl, file); + + // then + verify(s3Template).upload(any(), anyString(), eq(inputStream), any(ObjectMetadata.class)); + } + + @Test + @DisplayName("이미지 업로드 실패 - 유효하지 않은 키") + void uploadImageFail1() { + // given + String imgUrl = "imgUrl"; + MultipartFile file = mock(MultipartFile.class); + + // when + Exception exception = assertThrows(CustomException.class, + () -> s3Handler.uploadImages(imgUrl, file) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.INVALID_IMAGE.getMessage()); + } + + @Test + @DisplayName("이미지 업로드 실패 - 유효하지 않은 이미지") + void uploadImageFail2() throws IOException { + // given + String imgUrl = "imgUrl/key"; + MultipartFile file = mock(MultipartFile.class); + given(file.getInputStream()).willThrow(IOException.class); + + // when + Exception exception = assertThrows(CustomException.class, + () -> s3Handler.uploadImages(imgUrl, file) + ); + + // then + assertThat(exception.getMessage()).isEqualTo(ErrorCode.INVALID_IMAGE.getMessage()); + } + + @Test + @DisplayName("여러 이미지 업로드 성공") + void uploadImagesSuccess() { + // given + List imgUrlList = List.of("imgUrl/key1", "imgUrl/key2"); + List files = List.of(mock(MultipartFile.class), mock(MultipartFile.class)); + + // when + s3Handler.uploadImages(imgUrlList, files); + + // then + // verify(s3Handler, times(imgUrlList.size())).uploadImages(anyString(), any(MultipartFile.class)); + } + + @Test + @DisplayName("이미지 삭제") + void deleteImage() { + // given + String imgUrl = "imgUrl/key"; + + // when + s3Handler.deleteImage(imgUrl); + + // then + verify(s3Template).deleteObject(any(), anyString()); + + } +} \ No newline at end of file From 58829c21c33c95d506988007340d25a2e89df888 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 16 Dec 2023 22:55:06 +0900 Subject: [PATCH 35/82] =?UTF-8?q?Test:=20serviceTest=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/admin/AuthServiceTest.java | 2 -- .../example/purebasketbe/domain/cart/CartServiceTest.java | 5 ++--- .../purebasketbe/domain/member/MemberServiceTest.java | 3 +-- .../purebasketbe/domain/recipe/RecipeServiceTest.java | 8 ++++---- .../com/example/purebasketbe/global/s3/S3HandlerTest.java | 7 +++---- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java b/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java index f8e9ca2..112ff0c 100644 --- a/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/admin/AuthServiceTest.java @@ -11,11 +11,9 @@ 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 static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; diff --git a/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java b/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java index c7d0310..23f6c2c 100644 --- a/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/cart/CartServiceTest.java @@ -178,14 +178,13 @@ class UpdateCart { @DisplayName("장바구니 업데이트 성공") void updateCartSuccess() { // given - Cart cart = mock(Cart.class); + Cart cart = Cart.builder().amount(1).build(); given(cartRepository.findByProductIdAndMember(productId, member)).willReturn(Optional.of(cart)); - // when cartService.updateCart(productId, requestDto, member); // then - verify(cart).changeAmount(requestDto); + assertThat(cart.getAmount()).isEqualTo(requestDto.amount()); } @Test diff --git a/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java b/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java index 2e0d057..469ed89 100644 --- a/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/member/MemberServiceTest.java @@ -16,9 +16,8 @@ import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; - import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; diff --git a/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java b/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java index fd627c5..53d0082 100644 --- a/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/recipe/RecipeServiceTest.java @@ -84,7 +84,7 @@ void getRecipeSuccess() { Long recipeId = 1L; List productList = new ArrayList<>(); Recipe recipe = Recipe.builder().name("test recipe").productList(productList).build(); - given(recipeRepository.findById(any())).willReturn(Optional.of(recipe)); + given(recipeRepository.findById(recipeId)).willReturn(Optional.of(recipe)); // when RecipeResponseDto result = recipeService.getRecipe(recipeId); @@ -100,7 +100,7 @@ void getRecipeSuccess() { void getRecipeFail() { // given Long recipeId = 1L; - given(recipeRepository.findById(any())).willReturn(Optional.empty()); + given(recipeRepository.findById(recipeId)).willReturn(Optional.empty()); // when Exception exception = assertThrows(CustomException.class, @@ -189,7 +189,7 @@ void deleteRecipeSuccess() { // given Long recipeId = 1L; Recipe recipe = Recipe.builder().build(); - given(recipeRepository.findById(any())).willReturn(Optional.of(recipe)); + given(recipeRepository.findById(recipeId)).willReturn(Optional.of(recipe)); // when recipeService.deleteRecipe(recipeId); @@ -204,7 +204,7 @@ void deleteRecipeSuccess() { void deleteRecipeFail() { // given Long recipeId = 1L; - given(recipeRepository.findById(any())).willReturn(Optional.empty()); + given(recipeRepository.findById(recipeId)).willReturn(Optional.empty()); // when Exception exception = assertThrows(CustomException.class, diff --git a/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java b/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java index fbe4ba9..d579a51 100644 --- a/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java +++ b/src/test/java/com/example/purebasketbe/global/s3/S3HandlerTest.java @@ -4,11 +4,9 @@ import com.example.purebasketbe.global.exception.ErrorCode; import io.awspring.cloud.s3.ObjectMetadata; import io.awspring.cloud.s3.S3Template; -import org.junit.jupiter.api.BeforeEach; 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.junit.jupiter.MockitoExtension; @@ -18,11 +16,12 @@ import java.io.InputStream; import java.util.List; -import static org.junit.jupiter.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class S3HandlerTest { From 28451182b48848920fad1e68193c728bb5ddee45 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 18 Dec 2023 00:49:06 +0900 Subject: [PATCH 36/82] =?UTF-8?q?Test:=20ProductServiceTest=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[#28]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductServiceTest.java | 550 ++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java diff --git a/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java b/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java new file mode 100644 index 0000000..96a675c --- /dev/null +++ b/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java @@ -0,0 +1,550 @@ +package com.example.purebasketbe.domain.product; + +import com.example.purebasketbe.domain.product.dto.ProductListResponseDto; +import com.example.purebasketbe.domain.product.dto.ProductRequestDto; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import com.example.purebasketbe.domain.product.entity.Event; +import com.example.purebasketbe.domain.product.entity.Image; +import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.s3.S3Handler; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +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.data.domain.*; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class ProductServiceTest { + + @Mock + ProductRepository productRepository; + @Mock + ImageRepository imageRepository; + @Mock + S3Handler s3Handler; + @InjectMocks + ProductService productService; + + int eventPageSize = 4; + int pageSize = 12; + + int eventPage; + int page; + + Product eventProduct; + Product normalProduct; + + @BeforeEach + void setProducts() { + eventProduct = Product.builder() + .name("event product") + .price(1000) + .stock(100) + .event(Event.DISCOUNT) + .discountRate(50) + .category("product") + .build(); + + normalProduct = Product.builder() + .name("normal product") + .price(2000) + .stock(1000) + .event(Event.NORMAL) + .discountRate(0) + .category("product") + .build(); + } + + @Nested + @DisplayName("전체 상품 조회") + class GetProducts { + + @Test + @DisplayName("조회 성공") + void getProductsSuccess() { + // given + eventPage = 0; + page = 0; + + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + given(productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, pageables.get(0))) + .willReturn(productsPages.get(0)); + given(productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageables.get(1))) + .willReturn(productsPages.get(1)); + + // when + ProductListResponseDto responseDto = productService.getProducts(eventPage, page); + + // then + assertThat(responseDto.eventProducts().stream() + .map(ProductResponseDto::name) + .findFirst()) + .hasValue(eventProduct.getName()); + assertThat(responseDto.products().stream() + .map(ProductResponseDto::name) + .findFirst()) + .hasValue(normalProduct.getName()); + } + + @Test + @DisplayName("조회 실패") + void getProductsFail() { + // given + eventPage = 2; + page = 0; + + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + given(productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, pageables.get(0))) + .willReturn(productsPages.get(0)); + given(productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageables.get(1))) + .willReturn(productsPages.get(1)); + + // when + ProductListResponseDto responseDto = productService.getProducts(eventPage, page); + + // then + assertThat(responseDto.eventProducts().getNumberOfElements()).isZero(); + } + } + + @Nested + @DisplayName("상품 검색 조회") + class searchProducts { + + @Test + @DisplayName("조회 성공, w/o category") + void searchProductsSuccessWithoutCategory() { + // given + String query = "event"; + String category = ""; + eventPage = 0; + page = 0; + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + when(productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.DISCOUNT, query, pageables.get(0))) + .thenAnswer(invocation -> { + if (eventProduct.getName().contains(query)) { + return productsPages.get(0); + } else { + return emptyPage(pageables.get(0)); + } + }); + when(productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.NORMAL, query, pageables.get(1))) + .thenAnswer(invocation -> { + if (normalProduct.getName().contains(query)) { + return productsPages.get(1); + } else { + return emptyPage(pageables.get(1)); + } + }); + + // when + ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); + + // then + assertThat(responseDto.eventProducts().stream() + .map(ProductResponseDto::name) + .findFirst()) + .hasValue(eventProduct.getName()); + assertThat(responseDto.products().getNumberOfElements()).isZero(); + } + + @Test + @DisplayName("조회 실패(검색 상품 없음), w/o category") + void searchProductFailWithoutCategory() { + // given + String query = "nothing"; + String category = ""; + eventPage = 0; + page = 0; + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + when(productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.DISCOUNT, query, pageables.get(0))) + .thenAnswer(invocation -> { + if (eventProduct.getName().contains(query)) { + return productsPages.get(0); + } else { + return emptyPage(pageables.get(0)); + } + }); + when(productRepository.findAllByDeletedAndEventAndNameContains( + false, Event.NORMAL, query, pageables.get(1))) + .thenAnswer(invocation -> { + if (normalProduct.getName().contains(query)) { + return productsPages.get(1); + } else { + return emptyPage(pageables.get(1)); + } + }); + + // when + ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); + + // then + assertThat(responseDto.eventProducts().getNumberOfElements()).isZero(); + assertThat(responseDto.products().getNumberOfElements()).isZero(); + } + + @Test + @DisplayName("조회 성공, w/ category") + void searchProductsSuccessWithCategory() { + // given + String query = "event"; + String category = "product"; + eventPage = 0; + page = 0; + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.DISCOUNT, category, query, pageables.get(0))) + .thenAnswer(invocation -> { + if (eventProduct.getName().contains(query) && eventProduct.getCategory().equals(category)) { + return productsPages.get(0); + } else { + return emptyPage(pageables.get(0)); + } + }); + when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.NORMAL, category, query, pageables.get(1))) + .thenAnswer(invocation -> { + if (normalProduct.getName().contains(query) && normalProduct.getCategory().equals(category)) { + return productsPages.get(1); + } else { + return emptyPage(pageables.get(1)); + } + }); + + // when + ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); + + // then + assertThat(responseDto.eventProducts().stream() + .map(ProductResponseDto::name) + .findFirst()) + .hasValue(eventProduct.getName()); + assertThat(responseDto.products().getNumberOfElements()).isZero(); + } + + @Test + @DisplayName("조회 실패(검색 상품 없음), w/ category") + void searchProductsFailWithCategory() { + // given + String query = "event"; + String category = "nothing"; + eventPage = 0; + page = 0; + setPrivateFieldOfService(); + List pageables = getPageables(eventPage, page); + List> productsPages = getProductsPages(eventPage, page, pageables); + + when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.DISCOUNT, category, query, pageables.get(0))) + .thenAnswer(invocation -> { + if (eventProduct.getName().contains(query) && eventProduct.getCategory().equals(category)) { + return productsPages.get(0); + } else { + return emptyPage(pageables.get(0)); + } + }); + when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + false, Event.NORMAL, category, query, pageables.get(1))) + .thenAnswer(invocation -> { + if (normalProduct.getName().contains(query) && normalProduct.getCategory().equals(category)) { + return productsPages.get(1); + } else { + return emptyPage(pageables.get(1)); + } + }); + + // when + ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); + + // then + assertThat(responseDto.eventProducts().getNumberOfElements()).isZero(); + assertThat(responseDto.products().getNumberOfElements()).isZero(); + } + } + + @Nested + @DisplayName("단일 상품 조회") + class GetProduct { + + @Test + @DisplayName("조회 성공") + void getProductSuccess() { + // given + Long productId = eventProduct.getId(); + + given(productRepository.findByIdAndDeleted(productId, false)) + .willReturn(Optional.ofNullable(eventProduct)); + given(imageRepository.findAllByProductId(productId)) + .willReturn(List.of()); + + // when + ProductResponseDto responseDto = productService.getProduct(productId); + + // then + assertThat(responseDto.name()).isEqualTo(eventProduct.getName()); + } + + @Test + @DisplayName("조회 실패") + void getProductFail() { + // given + Long productId = 3L; + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> productService.getProduct(productId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + } + + @Nested + @DisplayName("상품 등록") + class RegisterProduct { + + @Test + @DisplayName("등록 성공") + void registerProductSuccess() { + // given + ProductRequestDto requestDto = new ProductRequestDto( + "test product", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + + given(productRepository.existsByName(any())).willReturn(false); + + // when + productService.registerProduct(requestDto, files); + + // then + verify(productRepository, times(1)).save(any()); + verify(imageRepository, times(1)).saveAll(any()); + verify(s3Handler, times(1)).uploadImages(anyList(), anyList()); + } + + @Test + @DisplayName("등록 실패(중복 상품)") + void registerProductFail() { + // given + ProductRequestDto requestDto = new ProductRequestDto( + "test product", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + + given(productRepository.existsByName(any())).willReturn(true); + + // when & then + assertThatThrownBy(() -> productService.registerProduct(requestDto, files)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.PRODUCT_ALREADY_EXISTS.getMessage()); + verify(productRepository, never()).save(any()); + verify(imageRepository, never()).saveAll(any()); + verify(s3Handler, never()).uploadImages(anyList(), anyList()); + } + } + + @Nested + @DisplayName("상품 수정") + class UpdateProduct { + + @Test + @DisplayName("수정 성공 w/ files") + void updateProductWithFilesSuccess() { + // given + Long productId = 1L; + ProductRequestDto requestDto = new ProductRequestDto( + "test update", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.ofNullable(normalProduct)); + + // when + productService.updateProduct(productId, requestDto, files); + + // then + assertThat(normalProduct.getName()).isEqualTo(requestDto.name()); + } + + @Test + @DisplayName("수정 실패(해당 상품 없음) w/ files") + void updateProductWithFilesFail() { + // given + Long productId = 1L; + ProductRequestDto requestDto = new ProductRequestDto( + "test update", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> productService.updateProduct(productId, requestDto, files)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("수정 성공 w/o files") + void updateProductWithoutFilesSuccess() { + // given + Long productId = 1L; + ProductRequestDto requestDto = new ProductRequestDto( + "test update", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of(); + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.ofNullable(normalProduct)); + + // when + productService.updateProduct(productId, requestDto, files); + + // then + assertThat(normalProduct.getName()).isEqualTo(requestDto.name()); + } + + @Test + @DisplayName("수정 실패(해당 상품 없음) w/o files") + void updateProductWithoutFilesFail() { + // given + Long productId = 1L; + ProductRequestDto requestDto = new ProductRequestDto( + "test update", 1000, 100, null, null, Event.NORMAL, 0 + ); + List files = List.of(); + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> productService.updateProduct(productId, requestDto, files)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + } + + @Nested + @DisplayName("상품 삭제") + class DeleteProduct { + + @Test + @DisplayName("삭제 성공") + void deleteProductSuccess() { + // given + Long productId = 1L; + List images = List.of(mock(Image.class)); + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.ofNullable(normalProduct)); + given(imageRepository.findAllByProductId(any())) + .willReturn(images); + + // when + productService.deleteProduct(productId); + + // then + assertThat(normalProduct.getName()).startsWith("normal product-deleted-"); + assertThat(normalProduct.isDeleted()).isTrue(); + verify(imageRepository, times(1)).findAllByProductId(any()); + verify(s3Handler, times(1)).deleteImage(any()); + verify(imageRepository, times(1)).deleteAllByProductId(any()); + } + + @Test + @DisplayName("삭제 실패(해당 상품 없음)") + void deleteProductFail() { + // given + Long productId = 1L; + + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> productService.deleteProduct(productId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.PRODUCT_NOT_FOUND.getMessage()); + } + } + + private void setPrivateFieldOfService() { + ReflectionTestUtils.setField(productService, "eventPageSize", eventPageSize); + ReflectionTestUtils.setField(productService, "pageSize", pageSize); + } + + private List getPageables(int eventPage, int page) { + Sort sort = Sort.by(Sort.Direction.DESC, "modifiedAt"); + Pageable eventPageable = PageRequest.of(eventPage, eventPageSize, sort); + Pageable pageable = PageRequest.of(page, pageSize, sort); + return List.of(eventPageable, pageable); + } + + private List> getProductsPages(int eventPage, int page, List pageables) { + List eventProductList = List.of(eventProduct); + List normalProductList = List.of(normalProduct); + Page eventProducts = new PageImpl<>(eventPage == 0 ? eventProductList : List.of(), pageables.get(0), eventProductList.size() / eventPageSize); + Page products = new PageImpl<>(page == 0 ? normalProductList : List.of(), pageables.get(1), normalProductList.size() / pageSize); + + return List.of(eventProducts, products); + } + + private Page emptyPage(Pageable pageable) { + return new PageImpl<>(List.of(), pageable, 1); + } + + private void printJsonResult(Object responseDto) { + ObjectMapper mapper = new ObjectMapper(); + try { + String jsonResponse = mapper.writeValueAsString(responseDto); + System.out.println(jsonResponse); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + +} From a5ff196263a0529d1818704cbb7f39b8bd471992 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 18 Dec 2023 02:48:39 +0900 Subject: [PATCH 37/82] =?UTF-8?q?Test:=20ProductServiceTest=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20[#28]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductServiceTest.java | 337 +++++------------- 1 file changed, 90 insertions(+), 247 deletions(-) diff --git a/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java b/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java index 96a675c..b8ba35a 100644 --- a/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java +++ b/src/test/java/com/example/purebasketbe/domain/product/ProductServiceTest.java @@ -9,7 +9,6 @@ import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.s3.S3Handler; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -47,88 +46,61 @@ public class ProductServiceTest { int eventPageSize = 4; int pageSize = 12; - int eventPage; - int page; - - Product eventProduct; - Product normalProduct; - - @BeforeEach - void setProducts() { - eventProduct = Product.builder() - .name("event product") - .price(1000) - .stock(100) - .event(Event.DISCOUNT) - .discountRate(50) - .category("product") - .build(); - - normalProduct = Product.builder() - .name("normal product") - .price(2000) - .stock(1000) - .event(Event.NORMAL) - .discountRate(0) - .category("product") - .build(); - } + Product product = Product.builder() + .name("normal product") + .price(2000) + .stock(1000) + .event(Event.NORMAL) + .discountRate(0) + .category("product") + .build(); @Nested @DisplayName("전체 상품 조회") class GetProducts { + int eventPage = 0; + int page = 0; + + Page products = getProductsPage(page); + + @BeforeEach + void setUp() { + setPrivateFieldOfService(); + } + @Test @DisplayName("조회 성공") void getProductsSuccess() { // given - eventPage = 0; - page = 0; - - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - given(productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, pageables.get(0))) - .willReturn(productsPages.get(0)); - given(productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageables.get(1))) - .willReturn(productsPages.get(1)); + given(productRepository.findAllByDeletedAndEvent( + any(Boolean.class), any(Event.class), any(Pageable.class))) + .willReturn(products); // when ProductListResponseDto responseDto = productService.getProducts(eventPage, page); // then - assertThat(responseDto.eventProducts().stream() - .map(ProductResponseDto::name) - .findFirst()) - .hasValue(eventProduct.getName()); assertThat(responseDto.products().stream() .map(ProductResponseDto::name) .findFirst()) - .hasValue(normalProduct.getName()); + .hasValue(product.getName()); } @Test - @DisplayName("조회 실패") + @DisplayName("조회 실패(상품 없음)") void getProductsFail() { // given - eventPage = 2; - page = 0; - - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - given(productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, pageables.get(0))) - .willReturn(productsPages.get(0)); - given(productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageables.get(1))) - .willReturn(productsPages.get(1)); + given(productRepository.findAllByDeletedAndEvent( + any(Boolean.class), any(Event.class), any(Pageable.class))) + .willReturn(Page.empty()); // when ProductListResponseDto responseDto = productService.getProducts(eventPage, page); // then assertThat(responseDto.eventProducts().getNumberOfElements()).isZero(); + assertThat(responseDto.products().getNumberOfElements()).isZero(); } } @@ -136,78 +108,43 @@ void getProductsFail() { @DisplayName("상품 검색 조회") class searchProducts { + String query = "event"; + String category = ""; + int eventPage = 0; + int page = 0; + + Page products = getProductsPage(page); + + @BeforeEach + void setUp() { + setPrivateFieldOfService(); + } + @Test @DisplayName("조회 성공, w/o category") void searchProductsSuccessWithoutCategory() { // given - String query = "event"; - String category = ""; - eventPage = 0; - page = 0; - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - when(productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.DISCOUNT, query, pageables.get(0))) - .thenAnswer(invocation -> { - if (eventProduct.getName().contains(query)) { - return productsPages.get(0); - } else { - return emptyPage(pageables.get(0)); - } - }); - when(productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.NORMAL, query, pageables.get(1))) - .thenAnswer(invocation -> { - if (normalProduct.getName().contains(query)) { - return productsPages.get(1); - } else { - return emptyPage(pageables.get(1)); - } - }); + given(productRepository.findAllByDeletedAndEventAndNameContains( + any(Boolean.class), any(Event.class), any(String.class), any(Pageable.class))) + .willReturn(products); // when ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); // then - assertThat(responseDto.eventProducts().stream() + assertThat(responseDto.products().stream() .map(ProductResponseDto::name) .findFirst()) - .hasValue(eventProduct.getName()); - assertThat(responseDto.products().getNumberOfElements()).isZero(); + .hasValue(product.getName()); } @Test @DisplayName("조회 실패(검색 상품 없음), w/o category") void searchProductFailWithoutCategory() { // given - String query = "nothing"; - String category = ""; - eventPage = 0; - page = 0; - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - when(productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.DISCOUNT, query, pageables.get(0))) - .thenAnswer(invocation -> { - if (eventProduct.getName().contains(query)) { - return productsPages.get(0); - } else { - return emptyPage(pageables.get(0)); - } - }); - when(productRepository.findAllByDeletedAndEventAndNameContains( - false, Event.NORMAL, query, pageables.get(1))) - .thenAnswer(invocation -> { - if (normalProduct.getName().contains(query)) { - return productsPages.get(1); - } else { - return emptyPage(pageables.get(1)); - } - }); + given(productRepository.findAllByDeletedAndEventAndNameContains( + any(Boolean.class), any(Event.class), any(String.class), any(Pageable.class))) + .willReturn(Page.empty()); // when ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); @@ -221,33 +158,11 @@ void searchProductFailWithoutCategory() { @DisplayName("조회 성공, w/ category") void searchProductsSuccessWithCategory() { // given - String query = "event"; - String category = "product"; - eventPage = 0; - page = 0; - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.DISCOUNT, category, query, pageables.get(0))) - .thenAnswer(invocation -> { - if (eventProduct.getName().contains(query) && eventProduct.getCategory().equals(category)) { - return productsPages.get(0); - } else { - return emptyPage(pageables.get(0)); - } - }); - when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.NORMAL, category, query, pageables.get(1))) - .thenAnswer(invocation -> { - if (normalProduct.getName().contains(query) && normalProduct.getCategory().equals(category)) { - return productsPages.get(1); - } else { - return emptyPage(pageables.get(1)); - } - }); + category = "test category"; + given(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + any(Boolean.class), any(Event.class), any(String.class), any(String.class), any(Pageable.class))) + .willReturn(products); // when ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); @@ -255,41 +170,18 @@ void searchProductsSuccessWithCategory() { assertThat(responseDto.eventProducts().stream() .map(ProductResponseDto::name) .findFirst()) - .hasValue(eventProduct.getName()); - assertThat(responseDto.products().getNumberOfElements()).isZero(); + .hasValue(product.getName()); } @Test @DisplayName("조회 실패(검색 상품 없음), w/ category") void searchProductsFailWithCategory() { // given - String query = "event"; - String category = "nothing"; - eventPage = 0; - page = 0; - setPrivateFieldOfService(); - List pageables = getPageables(eventPage, page); - List> productsPages = getProductsPages(eventPage, page, pageables); - - when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.DISCOUNT, category, query, pageables.get(0))) - .thenAnswer(invocation -> { - if (eventProduct.getName().contains(query) && eventProduct.getCategory().equals(category)) { - return productsPages.get(0); - } else { - return emptyPage(pageables.get(0)); - } - }); - when(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( - false, Event.NORMAL, category, query, pageables.get(1))) - .thenAnswer(invocation -> { - if (normalProduct.getName().contains(query) && normalProduct.getCategory().equals(category)) { - return productsPages.get(1); - } else { - return emptyPage(pageables.get(1)); - } - }); + category = "test category"; + given(productRepository.findAllByDeletedAndEventAndCategoryAndNameContains( + any(Boolean.class), any(Event.class), any(String.class), any(String.class), any(Pageable.class))) + .willReturn(Page.empty()); // when ProductListResponseDto responseDto = productService.searchProducts(query, category, eventPage, page); @@ -303,30 +195,28 @@ void searchProductsFailWithCategory() { @DisplayName("단일 상품 조회") class GetProduct { + Long productId = 1L; + @Test @DisplayName("조회 성공") void getProductSuccess() { // given - Long productId = eventProduct.getId(); - - given(productRepository.findByIdAndDeleted(productId, false)) - .willReturn(Optional.ofNullable(eventProduct)); - given(imageRepository.findAllByProductId(productId)) + given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) + .willReturn(Optional.ofNullable(product)); + given(imageRepository.findAllByProductId(any())) .willReturn(List.of()); // when ProductResponseDto responseDto = productService.getProduct(productId); // then - assertThat(responseDto.name()).isEqualTo(eventProduct.getName()); + assertThat(responseDto.name()).isEqualTo(product.getName()); } @Test @DisplayName("조회 실패") void getProductFail() { // given - Long productId = 3L; - given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) .willReturn(Optional.empty()); @@ -341,16 +231,15 @@ void getProductFail() { @DisplayName("상품 등록") class RegisterProduct { + ProductRequestDto requestDto = new ProductRequestDto( + "test product", 1000, 100, null, null, Event.NORMAL, 0); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + @Test @DisplayName("등록 성공") void registerProductSuccess() { // given - ProductRequestDto requestDto = new ProductRequestDto( - "test product", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of( - new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); - given(productRepository.existsByName(any())).willReturn(false); // when @@ -366,12 +255,6 @@ void registerProductSuccess() { @DisplayName("등록 실패(중복 상품)") void registerProductFail() { // given - ProductRequestDto requestDto = new ProductRequestDto( - "test product", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of( - new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); - given(productRepository.existsByName(any())).willReturn(true); // when & then @@ -388,38 +271,30 @@ void registerProductFail() { @DisplayName("상품 수정") class UpdateProduct { + Long productId = 1L; + ProductRequestDto requestDto = new ProductRequestDto( + "test update", 1000, 100, null, null, Event.NORMAL, 0); + List files = List.of( + new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); + @Test @DisplayName("수정 성공 w/ files") void updateProductWithFilesSuccess() { // given - Long productId = 1L; - ProductRequestDto requestDto = new ProductRequestDto( - "test update", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of( - new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); - given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) - .willReturn(Optional.ofNullable(normalProduct)); + .willReturn(Optional.ofNullable(product)); // when productService.updateProduct(productId, requestDto, files); // then - assertThat(normalProduct.getName()).isEqualTo(requestDto.name()); + assertThat(product.getName()).isEqualTo(requestDto.name()); } @Test @DisplayName("수정 실패(해당 상품 없음) w/ files") void updateProductWithFilesFail() { // given - Long productId = 1L; - ProductRequestDto requestDto = new ProductRequestDto( - "test update", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of( - new MockMultipartFile("image_name", "image_name.jpg", "image/jpeg", new byte[0])); - given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) .willReturn(Optional.empty()); @@ -433,31 +308,23 @@ void updateProductWithFilesFail() { @DisplayName("수정 성공 w/o files") void updateProductWithoutFilesSuccess() { // given - Long productId = 1L; - ProductRequestDto requestDto = new ProductRequestDto( - "test update", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of(); + files = List.of(); given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) - .willReturn(Optional.ofNullable(normalProduct)); + .willReturn(Optional.ofNullable(product)); // when productService.updateProduct(productId, requestDto, files); // then - assertThat(normalProduct.getName()).isEqualTo(requestDto.name()); + assertThat(product.getName()).isEqualTo(requestDto.name()); } @Test @DisplayName("수정 실패(해당 상품 없음) w/o files") void updateProductWithoutFilesFail() { // given - Long productId = 1L; - ProductRequestDto requestDto = new ProductRequestDto( - "test update", 1000, 100, null, null, Event.NORMAL, 0 - ); - List files = List.of(); + files = List.of(); given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) .willReturn(Optional.empty()); @@ -473,15 +340,15 @@ void updateProductWithoutFilesFail() { @DisplayName("상품 삭제") class DeleteProduct { + Long productId = 1L; + List images = List.of(mock(Image.class)); + @Test @DisplayName("삭제 성공") void deleteProductSuccess() { // given - Long productId = 1L; - List images = List.of(mock(Image.class)); - given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) - .willReturn(Optional.ofNullable(normalProduct)); + .willReturn(Optional.ofNullable(product)); given(imageRepository.findAllByProductId(any())) .willReturn(images); @@ -489,8 +356,8 @@ void deleteProductSuccess() { productService.deleteProduct(productId); // then - assertThat(normalProduct.getName()).startsWith("normal product-deleted-"); - assertThat(normalProduct.isDeleted()).isTrue(); + assertThat(product.getName()).startsWith("normal product-deleted-"); + assertThat(product.isDeleted()).isTrue(); verify(imageRepository, times(1)).findAllByProductId(any()); verify(s3Handler, times(1)).deleteImage(any()); verify(imageRepository, times(1)).deleteAllByProductId(any()); @@ -500,8 +367,6 @@ void deleteProductSuccess() { @DisplayName("삭제 실패(해당 상품 없음)") void deleteProductFail() { // given - Long productId = 1L; - given(productRepository.findByIdAndDeleted(any(), any(Boolean.class))) .willReturn(Optional.empty()); @@ -517,34 +382,12 @@ private void setPrivateFieldOfService() { ReflectionTestUtils.setField(productService, "pageSize", pageSize); } - private List getPageables(int eventPage, int page) { + private Page getProductsPage(int page) { Sort sort = Sort.by(Sort.Direction.DESC, "modifiedAt"); - Pageable eventPageable = PageRequest.of(eventPage, eventPageSize, sort); Pageable pageable = PageRequest.of(page, pageSize, sort); - return List.of(eventPageable, pageable); - } - - private List> getProductsPages(int eventPage, int page, List pageables) { - List eventProductList = List.of(eventProduct); - List normalProductList = List.of(normalProduct); - Page eventProducts = new PageImpl<>(eventPage == 0 ? eventProductList : List.of(), pageables.get(0), eventProductList.size() / eventPageSize); - Page products = new PageImpl<>(page == 0 ? normalProductList : List.of(), pageables.get(1), normalProductList.size() / pageSize); - - return List.of(eventProducts, products); + List productList = List.of(product); + return new PageImpl<>(page == 0 ? productList : List.of(), + pageable, + productList.size() / pageSize); } - - private Page emptyPage(Pageable pageable) { - return new PageImpl<>(List.of(), pageable, 1); - } - - private void printJsonResult(Object responseDto) { - ObjectMapper mapper = new ObjectMapper(); - try { - String jsonResponse = mapper.writeValueAsString(responseDto); - System.out.println(jsonResponse); - } catch (Exception e) { - System.out.println(e.getMessage()); - } - } - } From cb2650354d76514e7e15832d5d675357c967857a Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Tue, 19 Dec 2023 11:54:44 +0900 Subject: [PATCH 38/82] =?UTF-8?q?Chore:=20Kafka=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=A3=BC=EC=9E=85=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index b236475..7fc7f20 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,9 @@ dependencies { //redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // Kafka + implementation 'org.springframework.kafka:spring-kafka' + // aws implementation 'io.awspring.cloud:spring-cloud-aws-starter:3.0.0' implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.0' From ae5dc9ee888f34d4822a9e6f7c96b46bdda4fa0b Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Tue, 19 Dec 2023 11:58:23 +0900 Subject: [PATCH 39/82] =?UTF-8?q?Feat:=20Kafka=20=EA=B8=B0=EB=B3=B8=20Pub/?= =?UTF-8?q?Sub=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberService.java | 8 +++++++ .../domain/product/ProductService.java | 12 +++++++++++ .../global/config/KafkaConfig.java | 21 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java index d79cd0e..3d75f10 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java @@ -6,9 +6,12 @@ import com.example.purebasketbe.global.exception.ErrorCode; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class MemberService { @@ -25,6 +28,11 @@ public void registerMember(SignupRequestDto requestDto) { memberRepository.save(member); } + @KafkaListener(topics = "event", groupId = "${spring.kafka.consumer.group-id}") + public void printMessage(String msg) { + log.info("Message from Kafka : {}", msg); + } + private void checkIfEmailExist(String email) { if (memberRepository.existsByEmail(email)) { throw new CustomException(ErrorCode.EMAIL_ALREADY_EXISTS); diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 58314d5..45602c4 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.*; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -23,11 +24,14 @@ public class ProductService { private final ProductRepository productRepository; private final ImageRepository imageRepository; private final S3Handler s3Handler; + private final KafkaTemplate kafkaTemplate; @Value("${products.event.page.size}") private int eventPageSize; @Value("${products.page.size}") private int pageSize; + final String TOPIC = "event"; + @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { @@ -82,6 +86,10 @@ public void registerProduct(ProductRequestDto requestDto, List fi productRepository.save(newProduct); saveAndUploadImage(newProduct, files); + + if (newProduct.getEvent().equals(Event.DISCOUNT)) { + kafkaTemplate.send(TOPIC, "New Event Raised"); + } } @Transactional @@ -92,6 +100,10 @@ public void updateProduct(Long productId, ProductRequestDto requestDto, List Date: Thu, 21 Dec 2023 13:32:29 +0900 Subject: [PATCH 40/82] =?UTF-8?q?Chore:=20JavaMailSender=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7fc7f20..bad96b1 100644 --- a/build.gradle +++ b/build.gradle @@ -45,12 +45,14 @@ dependencies { implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - // prometheus grafana implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-core' implementation 'io.micrometer:micrometer-registry-prometheus' + // email service + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' From 57cfd8455661b938f2525a6182e4036cbf084669 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 21 Dec 2023 13:33:31 +0900 Subject: [PATCH 41/82] =?UTF-8?q?Feat:=20Kafka=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=ED=81=90=20=EA=B5=AC=ED=98=84=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/MemberRepository.java | 6 ++++- .../domain/member/MemberService.java | 27 ++++++++++++++++--- .../domain/product/ProductService.java | 27 ++++++++++--------- .../product/dto/ProductResponseDto.java | 17 +++++++++--- .../global/kafka/KafkaService.java | 27 +++++++++++++++++++ .../global/tool/EmailContents.java | 25 +++++++++++++++++ 6 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java create mode 100644 src/main/java/com/example/purebasketbe/global/tool/EmailContents.java diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java index f2a35b9..60704c4 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java @@ -1,10 +1,11 @@ package com.example.purebasketbe.domain.member; import com.example.purebasketbe.domain.member.entity.Member; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository @@ -12,4 +13,7 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); + + @Query("select m.email from Member m ") + List findAllEmails(); } diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java index 3d75f10..11f910c 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java @@ -2,21 +2,30 @@ import com.example.purebasketbe.domain.member.dto.SignupRequestDto; import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.tool.EmailContents; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.List; + +import static com.example.purebasketbe.global.kafka.KafkaService.TOPIC_EVENT; + @Slf4j @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; + private final JavaMailSender javaMailSender; @Transactional public void registerMember(SignupRequestDto requestDto) { @@ -28,9 +37,21 @@ public void registerMember(SignupRequestDto requestDto) { memberRepository.save(member); } - @KafkaListener(topics = "event", groupId = "${spring.kafka.consumer.group-id}") - public void printMessage(String msg) { - log.info("Message from Kafka : {}", msg); + @KafkaListener(topics = TOPIC_EVENT, groupId = "${spring.kafka.consumer.group-id}") + public void sendEmailToMembers(ProductResponseDto responseDto) { + log.info("method called : sendEmailToMembers"); + List emailList = memberRepository.findAllEmails(); + EmailContents contents = EmailContents.from(responseDto); + String subject = contents.subject(); + String text = contents.text(); + + for (String email : emailList) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setTo(email); + message.setSubject(subject); + message.setText(text); + javaMailSender.send(message); + } } private void checkIfEmailExist(String email) { diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 45602c4..b120c38 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -1,16 +1,21 @@ package com.example.purebasketbe.domain.product; -import com.example.purebasketbe.domain.product.dto.*; +import com.example.purebasketbe.domain.product.dto.ProductListResponseDto; +import com.example.purebasketbe.domain.product.dto.ProductRequestDto; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Image; import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.*; -import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -24,7 +29,7 @@ public class ProductService { private final ProductRepository productRepository; private final ImageRepository imageRepository; private final S3Handler s3Handler; - private final KafkaTemplate kafkaTemplate; + private final KafkaService kafkaHandler; @Value("${products.event.page.size}") private int eventPageSize; @@ -87,9 +92,8 @@ public void registerProduct(ProductRequestDto requestDto, List fi productRepository.save(newProduct); saveAndUploadImage(newProduct, files); - if (newProduct.getEvent().equals(Event.DISCOUNT)) { - kafkaTemplate.send(TOPIC, "New Event Raised"); - } + if (newProduct.getEvent().equals(Event.DISCOUNT)) + kafkaHandler.sendEventToKafka(ProductResponseDto.from(newProduct)); } @Transactional @@ -97,13 +101,10 @@ public void updateProduct(Long productId, ProductRequestDto requestDto, List imgUrlList) { .images(imgUrlList) .build(); } + + public static ProductResponseDto from(Product product) { + return ProductResponseDto.builder() + .id(product.getId()) + .name(product.getName()) + .price(product.getPrice()) + .stock(product.getStock()) + .info(product.getInfo()) + .category(product.getCategory()) + .event(product.getEvent()) + .discountRate(product.getDiscountRate()) + .build(); + } } diff --git a/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java b/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java new file mode 100644 index 0000000..e437501 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java @@ -0,0 +1,27 @@ +package com.example.purebasketbe.global.kafka; + +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KafkaService { + + private final KafkaTemplate kafkaTemplate; + + public static final String TOPIC_EVENT = "sale_event"; + + // Methods for Producer + public void sendEventToKafka(ProductResponseDto responseDto) { + kafkaTemplate.send(TOPIC_EVENT, responseDto); + } + + // Consumers + // MemberService - sendEmailToMembers() + // SseService - alarmNewEvent() + +} diff --git a/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java b/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java new file mode 100644 index 0000000..0188739 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java @@ -0,0 +1,25 @@ +package com.example.purebasketbe.global.tool; + +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import lombok.Builder; + +public record EmailContents( + String subject, + String text +) { + @Builder + public EmailContents { + } + + public static EmailContents from(ProductResponseDto responseDto) { + int salePrice = responseDto.price() * (100 - responseDto.discountRate()) / 100; + return EmailContents.builder() + .subject("(광고) Pure Basket의 새로운 할인 이벤트가 시작됩니다!") + .text(String.format(""" + %s 할인 이벤트! + 어디에도 없을 가격 %d원!! + 한정 수량 %d개 쏩니다!!!""", + responseDto.name(), salePrice, responseDto.stock()) + ).build(); + } +} From bdc7396e9b6620a7050899a1eb7a6253b1cfeda6 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 21 Dec 2023 13:34:07 +0900 Subject: [PATCH 42/82] =?UTF-8?q?Feat:=20SSE=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC=ED=98=84=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/sse/SseController.java | 24 ++++++ .../global/sse/SseRepository.java | 26 ++++++ .../purebasketbe/global/sse/SseService.java | 80 +++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 src/main/java/com/example/purebasketbe/global/sse/SseController.java create mode 100644 src/main/java/com/example/purebasketbe/global/sse/SseRepository.java create mode 100644 src/main/java/com/example/purebasketbe/global/sse/SseService.java diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseController.java b/src/main/java/com/example/purebasketbe/global/sse/SseController.java new file mode 100644 index 0000000..5bae4e8 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/sse/SseController.java @@ -0,0 +1,24 @@ +package com.example.purebasketbe.global.sse; + +import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.global.tool.LoginAccount; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/sse") +public class SseController { + + private final SseService sseService; + + @PostMapping + public SseEmitter subscribe(@LoginAccount Member member, + @RequestHeader(value = "Last-Event-ID", required = false) String lastEventId) { + return sseService.subscribe(member.getEmail(), lastEventId); + } +} diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseRepository.java b/src/main/java/com/example/purebasketbe/global/sse/SseRepository.java new file mode 100644 index 0000000..978685c --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/sse/SseRepository.java @@ -0,0 +1,26 @@ +package com.example.purebasketbe.global.sse; + +import org.springframework.stereotype.Repository; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class SseRepository { + + private final Map connectMap = new ConcurrentHashMap<>(); + + public SseEmitter save(String emitterId, SseEmitter emitter) { + connectMap.put(emitterId, emitter); + return emitter; + } + + public void delete(String emitterId) { + connectMap.remove(emitterId); + } + + public Map findAllEmitters() { + return connectMap; + } +} diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseService.java b/src/main/java/com/example/purebasketbe/global/sse/SseService.java new file mode 100644 index 0000000..d2ad089 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/sse/SseService.java @@ -0,0 +1,80 @@ +package com.example.purebasketbe.global.sse; + +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.example.purebasketbe.global.kafka.KafkaService.TOPIC_EVENT; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SseService { + + private final SseRepository sseRepository; + + private final long DEFAULT_TIMEOUT = 60L * 60 * 1000; + + public SseEmitter subscribe(String email, String lastEventId) { + String emitterId = email + "_" + System.currentTimeMillis(); + SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); + sseRepository.save(emitterId, emitter); + + notify(emitter, emitterId, "connected!"); // 503 에러방지 더미 데이터 + + emitter.onCompletion(() -> { + log.info("onCompletion callback"); + sseRepository.delete(emitterId); + }); + return emitter; + } + + @KafkaListener(topics = TOPIC_EVENT, groupId = "${spring.kafka.consumer.group-id}") + private void alarmNewEvent(ProductResponseDto responseDto) { + Map connectionMap = sseRepository.findAllEmitters(); + int salePrice = responseDto.price() * responseDto.discountRate() / 100; + String message = String.format(""" + 새로운 할인 이벤트! + %s %d%% 할인!! 한정수량 단 %d개!!!""", + responseDto.name(), responseDto.discountRate(), responseDto.stock()); + + if (!connectionMap.isEmpty()) { + connectionMap.forEach((id, emitter) -> notify(emitter, id, message)); + } + } + + private void notify(SseEmitter emitter, String emitterId, Object data) { + try { + emitter.send(SseEmitter.event() + .id(emitterId) + .data(data)); // String Type만 가능함 + } catch (IOException e) { + log.error("notify 실패 : {}", e.getMessage()); + sseRepository.delete(emitterId); + } + } + + @Scheduled(fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3분마다 heartbeat 메세지 전달. + public void sendHeartbeat() { + Map connectionMap = sseRepository.findAllEmitters(); + if (!connectionMap.isEmpty()) { + connectionMap.forEach((key, emitter) -> { + try { + emitter.send(SseEmitter.event().id(key).name("heartbeat").data("heartbeat")); + log.info("하트비트 메세지 전송"); + } catch (IOException e) { + sseRepository.delete(key); + log.error("하트비트 전송 실패: {}", e.getMessage()); + } + }); + } + } +} From a2066de9f225eba81fb4579215697b5d534c7e45 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 21 Dec 2023 13:34:41 +0900 Subject: [PATCH 43/82] =?UTF-8?q?Update:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=84=A4?= =?UTF-8?q?=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/global/tool/LoginAccount.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java b/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java index 540dccc..e782628 100644 --- a/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java +++ b/src/main/java/com/example/purebasketbe/global/tool/LoginAccount.java @@ -6,6 +6,17 @@ import java.lang.annotation.Target; import org.springframework.security.core.annotation.AuthenticationPrincipal; +/** + * Custom Annotation that is used to resolve {@link com.example.purebasketbe.global.security.impl.UserDetailsImpl#getUser()} to a method + * argument by implementing {@link AuthenticationPrincipal#expression()} + * + * @author Sanghyu Lee + * + * See: + * "@AuthenticationPrincipal" + */ + @SuppressWarnings("ALL") @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) From a44b66fa472ac122d7e1858da9f4bb1c42518b07 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Thu, 21 Dec 2023 13:35:11 +0900 Subject: [PATCH 44/82] =?UTF-8?q?Feat:=20Swagger=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SwaggerConfig.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/com/example/purebasketbe/global/config/SwaggerConfig.java diff --git a/src/main/java/com/example/purebasketbe/global/config/SwaggerConfig.java b/src/main/java/com/example/purebasketbe/global/config/SwaggerConfig.java new file mode 100644 index 0000000..14979b7 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/SwaggerConfig.java @@ -0,0 +1,33 @@ +package com.example.purebasketbe.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.persistence.criteria.CriteriaBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private final String SCHEME_NAME = "BearerAuth"; + // Bearer 빼고 붙여 넣으면 됨 + @Bean + public OpenAPI customOAS() { + return new OpenAPI().components( + new Components().addSecuritySchemes( + SCHEME_NAME, new SecurityScheme() + .type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"))) + .addSecurityItem(new SecurityRequirement().addList(SCHEME_NAME)) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("Pure Basket E-commerce Project") + .description("HH-Final-6") + .version("0.1.0"); + } +} From 13fce9258c87ba0a6f1665106f278fc544ac4a66 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 22 Dec 2023 10:46:48 +0900 Subject: [PATCH 45/82] =?UTF-8?q?Update:=20ProductService=20deleteProduct?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductService.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 58314d5..e1374fb 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -1,6 +1,8 @@ package com.example.purebasketbe.domain.product; -import com.example.purebasketbe.domain.product.dto.*; +import com.example.purebasketbe.domain.product.dto.ProductListResponseDto; +import com.example.purebasketbe.domain.product.dto.ProductRequestDto; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Image; import com.example.purebasketbe.domain.product.entity.Product; @@ -9,12 +11,16 @@ import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.*; +import java.util.ArrayList; +import java.util.List; @Service @RequiredArgsConstructor @@ -98,9 +104,9 @@ public void updateProduct(Long productId, ProductRequestDto requestDto, List s3Handler.deleteImage(image.getImgUrl())); + List imageList = imageRepository.findAllByProductId(productId); imageRepository.deleteAllByProductId(productId); + imageList.forEach(image -> s3Handler.deleteImage(image.getImgUrl())); } private Page getResponseDtoFromProducts(Page products) { From caac85c21bcd36474fbe8a1f5859d15eb67f4775 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 22 Dec 2023 10:47:28 +0900 Subject: [PATCH 46/82] =?UTF-8?q?Chore:=20jacoco=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 33 ++++++++++++++++++--------------- lombok.config | 1 + 2 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 lombok.config diff --git a/build.gradle b/build.gradle index b236475..54d804a 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ tasks.named('test') { } test { - useJUnitPlatform() + useJUnitPlatform() // JUnit5 finalizedBy jacocoTestReport // report is always generated after tests run } @@ -74,17 +74,24 @@ jacoco { // JaCoCo 버전 toolVersion = '0.8.9' reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') - } jacocoTestReport { dependsOn test // tests are required to run before generating the report reports { - xml.required = false - csv.required = false - html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + html.destination file('build/reports/testReport.html') finalizedBy 'jacocoTestCoverageVerification' + } + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, exclude: [ + '**/PureBasketBeApplication', + '**/security/*', + '**/*dto*', + '**/config/*' + ]) + })) } } @@ -94,17 +101,13 @@ jacocoTestCoverageVerification { enabled = true element = 'CLASS' - // 라인 커버리지 제한을 70%로 설정 -// limit { -// counter = 'LINE' -// value = 'COVEREDRATIO' -// minimum = 0.70 -// } + excludes = [ + '**.*PureBasketBeApplication*', + '**.*security*', + '**.*dto*', + '**.*config*', + ] } - - rule { - // 규칙을 여러개 추가할 수 있습니다. - } } } \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..8f7e8aa --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file From f62d3f65a72a1b9f6d64584266243fd58bc5cece Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 22 Dec 2023 13:47:49 +0900 Subject: [PATCH 47/82] =?UTF-8?q?Fix:=20Landing=20page=20N+1=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/product/ProductRepository.java | 1 - .../purebasketbe/domain/product/ProductService.java | 5 +---- .../purebasketbe/domain/product/entity/Product.java | 8 ++++++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index f66310e..a89ae3a 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -22,7 +22,6 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet Page findAllByDeletedAndEventAndNameContains(boolean isDeleted, Event event, String query, Pageable pageable); - List findByIdIn(List requestIds); Optional findByIdAndDeleted(Long productId, boolean isDeleted); diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index e1374fb..4497781 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -122,10 +122,7 @@ private Pageable getPageable(int page, int pageSize) { } private List getImgUrlList(Product product) { - return imageRepository.findAllByProductId(product.getId()) - .stream() - .map(Image::getImgUrl) - .toList(); + return product.getImages().stream().map(Image::getImgUrl).toList(); } private void saveAndUploadImage(Product product, List files) { diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index daf753c..e589823 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -6,6 +6,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -53,8 +55,10 @@ public class Product { @Column(nullable = false) private boolean deleted; - @OneToMany(mappedBy = "product") - private List images = new ArrayList<>();; + @Fetch(FetchMode.SUBSELECT) + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private List images = new ArrayList<>(); + @Builder From d5cc7ba620c1f6d9dc6e4101b75103c008fa7f94 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Sat, 23 Dec 2023 10:51:50 +0900 Subject: [PATCH 48/82] =?UTF-8?q?Update:=20Sse=20=EC=88=98=EC=A0=95,=20Sch?= =?UTF-8?q?eduling=20=EC=A0=81=EC=9A=A9=20[#30]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSE GetMapping 적용 Application @EnableScheduling 적용 --- .../com/example/purebasketbe/PureBasketBeApplication.java | 3 ++- .../com/example/purebasketbe/global/sse/SseController.java | 7 ++----- .../com/example/purebasketbe/global/sse/SseService.java | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java index 9fb75b3..7a1963d 100644 --- a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java +++ b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java @@ -2,9 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @EnableJpaAuditing @SpringBootApplication public class PureBasketBeApplication { diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseController.java b/src/main/java/com/example/purebasketbe/global/sse/SseController.java index 5bae4e8..9378051 100644 --- a/src/main/java/com/example/purebasketbe/global/sse/SseController.java +++ b/src/main/java/com/example/purebasketbe/global/sse/SseController.java @@ -3,10 +3,7 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.global.tool.LoginAccount; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController @@ -16,7 +13,7 @@ public class SseController { private final SseService sseService; - @PostMapping + @GetMapping public SseEmitter subscribe(@LoginAccount Member member, @RequestHeader(value = "Last-Event-ID", required = false) String lastEventId) { return sseService.subscribe(member.getEmail(), lastEventId); diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseService.java b/src/main/java/com/example/purebasketbe/global/sse/SseService.java index d2ad089..4e0165f 100644 --- a/src/main/java/com/example/purebasketbe/global/sse/SseService.java +++ b/src/main/java/com/example/purebasketbe/global/sse/SseService.java @@ -28,7 +28,7 @@ public SseEmitter subscribe(String email, String lastEventId) { SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); sseRepository.save(emitterId, emitter); - notify(emitter, emitterId, "connected!"); // 503 에러방지 더미 데이터 + sendHeartbeat(); // 503 에러방지 더미 데이터 emitter.onCompletion(() -> { log.info("onCompletion callback"); From c99fe4a99fc4440e128340f31aa8583174ae4ab4 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 25 Dec 2023 13:40:17 +0900 Subject: [PATCH 49/82] =?UTF-8?q?Feat:=20Redis=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductService.java | 4 ++ .../global/config/RedisCacheConfig.java | 66 +++++++++++++++++++ .../global/config/RedisConfig.java | 14 ++-- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 1daf713..9ddb10b 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -11,7 +11,9 @@ import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -25,6 +27,7 @@ @Service @RequiredArgsConstructor +@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; @@ -39,6 +42,7 @@ public class ProductService { final String TOPIC = "event"; + @Cacheable(value = "products", key = "#eventPage + '_' + #page") @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { Pageable eventPageable = getPageable(eventPage, eventPageSize); diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java new file mode 100644 index 0000000..31a95b3 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java @@ -0,0 +1,66 @@ +package com.example.purebasketbe.global.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + @Value("${spring.cache.redis.host}") + private String cacheHost; + + @Value("${spring.cache.redis.port}") + private int cachePort; + + @Bean(name = "redisCacheConnectionFactory") + public RedisConnectionFactory redisCacheConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(cacheHost); + redisStandaloneConfiguration.setPort(cachePort); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean(name = "redisCacheTemplate") + public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } + + @Bean + public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { + RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()).build(); + return redisCacheManager; + } + + private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { + return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(300)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java index e53b288..494e676 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -1,10 +1,13 @@ package com.example.purebasketbe.global.config; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -14,12 +17,15 @@ @Configuration @EnableRedisRepositories public class RedisConfig { - private final RedisProperties redisProperties; + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private int port; - // RedisProperties로 yaml에 저장한 host, post를 연결 @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + @Primary + public LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); } // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 From 200220797661aa8a915faee3c0188778b0b2b811 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 25 Dec 2023 22:18:17 +0900 Subject: [PATCH 50/82] =?UTF-8?q?Fix:=20Redis=20cache=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=8B=9C=20SerializationException=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/PureBasketBeApplication.java | 2 ++ .../domain/product/ProductRepository.java | 6 ++-- .../domain/product/ProductService.java | 10 +++---- .../purebasketbe/global/RestPageImpl.java | 29 +++++++++++++++++++ .../global/config/RedisCacheConfig.java | 7 ++--- 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/RestPageImpl.java diff --git a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java index 7a1963d..bb7c46e 100644 --- a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java +++ b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java @@ -2,9 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableCaching @EnableScheduling @EnableJpaAuditing @SpringBootApplication diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index a89ae3a..29fb1a5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -4,12 +4,12 @@ import com.example.purebasketbe.domain.product.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.List; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface ProductRepository extends JpaRepository { diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 9ddb10b..e4c4d89 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -6,12 +6,12 @@ import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Image; import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.global.RestPageImpl; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; @@ -27,7 +27,6 @@ @Service @RequiredArgsConstructor -@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; @@ -48,13 +47,14 @@ public ProductListResponseDto getProducts(int eventPage, int page) { Pageable eventPageable = getPageable(eventPage, eventPageSize); Pageable pageable = getPageable(page, pageSize); + Page eventProducts = productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, eventPageable); Page products = productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageable); - Page eventProductsResponse = getResponseDtoFromProducts(eventProducts); - Page productsResponse = getResponseDtoFromProducts(products); + RestPageImpl eventProductsRestPage = RestPageImpl.from(getResponseDtoFromProducts(eventProducts)); + RestPageImpl productsRestPage = RestPageImpl.from(getResponseDtoFromProducts(products)); - return ProductListResponseDto.of(eventProductsResponse, productsResponse); + return ProductListResponseDto.of(eventProductsRestPage, productsRestPage); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/purebasketbe/global/RestPageImpl.java b/src/main/java/com/example/purebasketbe/global/RestPageImpl.java new file mode 100644 index 0000000..5bc497f --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/RestPageImpl.java @@ -0,0 +1,29 @@ +package com.example.purebasketbe.global; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"}) +public class RestPageImpl extends PageImpl { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private RestPageImpl(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + private RestPageImpl(Page page) { + super(page.getContent(), page.getPageable(), page.getTotalElements()); + } + + public static RestPageImpl from(Page page) { + return new RestPageImpl(page); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java index 31a95b3..97e5e41 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java @@ -3,9 +3,9 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; @@ -17,7 +17,6 @@ import java.time.Duration; -@EnableCaching @Configuration public class RedisCacheConfig { @Value("${spring.cache.redis.host}") @@ -52,8 +51,8 @@ public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") return redisCacheManager; } - private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { - return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() + private RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(300)) .disableCachingNullValues() .serializeKeysWith( From abe0d408f6cde29ab9cac7a8cd5c3bdc7c3a8139 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 27 Dec 2023 11:20:40 +0900 Subject: [PATCH 51/82] =?UTF-8?q?Fix:=20N+1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Product와 Cart, Product와 Purchase에서 발생하는 N+1 문제 해결 --- .../domain/cart/CartRepository.java | 4 +--- .../purebasketbe/domain/cart/CartService.java | 7 ++----- .../domain/cart/dto/CartResponseDto.java | 18 ++++++++---------- .../domain/product/ProductRepository.java | 6 +++--- .../domain/product/entity/Product.java | 7 ++----- .../domain/purchase/entity/Purchase.java | 7 +------ 6 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index cf932b2..ea38c1c 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -21,9 +21,7 @@ public interface CartRepository extends JpaRepository { @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") void deleteByUserAndProductIn(Member member, List products); - @Query("SELECT c FROM Cart c " + - "JOIN FETCH c.product " + - "WHERE c.member = :member") + @Query("SELECT c FROM Cart c JOIN FETCH c.product WHERE c.member = :member") List findAllByMember(Member member); void deleteAllByMemberAndProductIn(Member member, List productList); diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index 48f2417..e89d7cf 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -40,11 +40,8 @@ public void addToCart(Long productId, CartRequestDto requestDto, Member member) @Transactional(readOnly = true) public List getCartList(Member member) { return cartRepository.findAllByMember(member).stream() - .map(cart -> { - Product product = findAndValidateProduct(cart.getProduct().getId()); - Image image = findImage(product.getId()); - return CartResponseDto.of(product, image, cart); - }).toList(); + .filter(cart -> !cart.getProduct().isDeleted()) + .map(CartResponseDto::from).toList(); } @Transactional diff --git a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java index 7288be8..c79ab6f 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java @@ -1,11 +1,7 @@ package com.example.purebasketbe.domain.cart.dto; import com.example.purebasketbe.domain.cart.entity.Cart; -import com.example.purebasketbe.domain.product.entity.Image; -import com.example.purebasketbe.domain.product.entity.Product; -import lombok.AccessLevel; import lombok.Builder; -import lombok.RequiredArgsConstructor; public record CartResponseDto( Long id, @@ -20,14 +16,16 @@ public record CartResponseDto( } - public static CartResponseDto of(Product product, Image image, Cart cart) { + + public static CartResponseDto from( Cart cart) { return CartResponseDto.builder() - .id(product.getId()) - .name(product.getName()) - .price(product.getPrice()) - .category(product.getCategory()) - .imageUrl(image.getImgUrl()) + .id(cart.getProduct().getId()) + .name(cart.getProduct().getName()) + .price(cart.getProduct().getPrice()) + .category(cart.getProduct().getCategory()) + .imageUrl(cart.getProduct().getImages().get(0).getImgUrl()) .amount(cart.getAmount()) .build(); } + } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index a89ae3a..29fb1a5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -4,12 +4,12 @@ import com.example.purebasketbe.domain.product.entity.Product; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import java.util.List; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface ProductRepository extends JpaRepository { diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index e589823..373b8c2 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -6,8 +6,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.Fetch; -import org.hibernate.annotations.FetchMode; +import org.hibernate.annotations.BatchSize; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; @@ -55,12 +54,10 @@ public class Product { @Column(nullable = false) private boolean deleted; - @Fetch(FetchMode.SUBSELECT) + @BatchSize(size = 21) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List images = new ArrayList<>(); - - @Builder private Product(String name, Integer price, Integer stock, String info, String category, Event event, Integer discountRate) { diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java index beb5747..0baf6b0 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java @@ -18,9 +18,6 @@ public class Purchase extends TimeStamp{ @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private String name; - @Min(value = 1) @Column(nullable = false) private int amount; @@ -37,8 +34,7 @@ public class Purchase extends TimeStamp{ private Product product; @Builder - private Purchase(String name, int amount, int price, Member member, Product product) { - this.name = name; + private Purchase(int amount, int price, Member member, Product product) { this.amount = amount; this.price = price; this.member = member; @@ -47,7 +43,6 @@ private Purchase(String name, int amount, int price, Member member, Product prod public static Purchase of(Product product, int amount, Member member) { return Purchase.builder() - .name(product.getName()) .amount(amount) .price(product.getPrice()) .member(member) From ad3b3689442f1c114bb86b8da1a234b2e75020e4 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 27 Dec 2023 11:23:13 +0900 Subject: [PATCH 52/82] =?UTF-8?q?Chore:=20Dockerfile=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 28a986f..ea8023f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,10 @@ COPY ${JAR_FILE} app.jar # jar 파일 실행 # 생성된 이미지를 컨테이너로 실행하는 시점에 app.jar 실행 # Duser.timezone : 타임 존 지정 -ENTRYPOINT ["java", "-jar", "-Duser.timezone=Asia/Seoul", "/app.jar"] +ENTRYPOINT ["java", "-jar",\ + "-Duser.timezone=Asia/Seoul",\ + "-Dspring.profiles.active=dev",\ + "-javaagent:./pinpoint/pinpoint-bootstrap-2.5.3.jar",\ + "-Dpinpoint.agentId=purebasket01","-Dpinpoint.applicationName=purebasket",\ + "-Dpinpoint.config=./pinpoint/pinpoint-root.config",\ + "/app.jar"] From 2bac5bbf297acf4be956e3d34350088ea374dc17 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 29 Dec 2023 10:20:05 +0900 Subject: [PATCH 53/82] =?UTF-8?q?Refactor:=20=EC=B9=B4=ED=94=84=EC=B9=B4?= =?UTF-8?q?=20consumer=20=EB=B6=84=EB=A6=AC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kafka producer/consumer 설정 변경 --- .../domain/cart/CartRepository.java | 2 +- .../domain/member/MemberRepository.java | 4 -- .../domain/member/MemberService.java | 26 --------- .../domain/product/ProductService.java | 8 +-- .../domain/product/dto/KafkaEventDto.java | 26 +++++++++ .../domain/purchase/dto/KafkaPurchaseDto.java | 19 +++++++ .../global/config/KafkaConfig.java | 55 ++++++++++++++++++- .../global/kafka/KafkaService.java | 21 ++++--- .../purebasketbe/global/sse/SseService.java | 52 +++++++++--------- .../global/tool/EmailContents.java | 25 --------- 10 files changed, 145 insertions(+), 93 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/dto/KafkaEventDto.java create mode 100644 src/main/java/com/example/purebasketbe/domain/purchase/dto/KafkaPurchaseDto.java delete mode 100644 src/main/java/com/example/purebasketbe/global/tool/EmailContents.java diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index ea38c1c..4b27f78 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -19,7 +19,7 @@ public interface CartRepository extends JpaRepository { @Modifying @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") - void deleteByUserAndProductIn(Member member, List products); + void deleteByMemberAndProductIn(Member member, List products); @Query("SELECT c FROM Cart c JOIN FETCH c.product WHERE c.member = :member") List findAllByMember(Member member); diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java index 60704c4..1eca485 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberRepository.java @@ -2,10 +2,8 @@ import com.example.purebasketbe.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; @Repository @@ -14,6 +12,4 @@ public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); - @Query("select m.email from Member m ") - List findAllEmails(); } diff --git a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java index 11f910c..cbd6259 100644 --- a/src/main/java/com/example/purebasketbe/domain/member/MemberService.java +++ b/src/main/java/com/example/purebasketbe/domain/member/MemberService.java @@ -2,30 +2,20 @@ import com.example.purebasketbe.domain.member.dto.SignupRequestDto; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.product.dto.ProductResponseDto; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; -import com.example.purebasketbe.global.tool.EmailContents; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.util.List; - -import static com.example.purebasketbe.global.kafka.KafkaService.TOPIC_EVENT; - @Slf4j @Service @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; - private final JavaMailSender javaMailSender; @Transactional public void registerMember(SignupRequestDto requestDto) { @@ -37,22 +27,6 @@ public void registerMember(SignupRequestDto requestDto) { memberRepository.save(member); } - @KafkaListener(topics = TOPIC_EVENT, groupId = "${spring.kafka.consumer.group-id}") - public void sendEmailToMembers(ProductResponseDto responseDto) { - log.info("method called : sendEmailToMembers"); - List emailList = memberRepository.findAllEmails(); - EmailContents contents = EmailContents.from(responseDto); - String subject = contents.subject(); - String text = contents.text(); - - for (String email : emailList) { - SimpleMailMessage message = new SimpleMailMessage(); - message.setTo(email); - message.setSubject(subject); - message.setText(text); - javaMailSender.send(message); - } - } private void checkIfEmailExist(String email) { if (memberRepository.existsByEmail(email)) { diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 1daf713..48b765a 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -36,8 +36,6 @@ public class ProductService { private int eventPageSize; @Value("${products.page.size}") private int pageSize; - final String TOPIC = "event"; - @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { @@ -93,8 +91,9 @@ public void registerProduct(ProductRequestDto requestDto, List fi productRepository.save(newProduct); saveAndUploadImage(newProduct, files); - if (newProduct.getEvent().equals(Event.DISCOUNT)) + if (newProduct.getEvent().equals(Event.DISCOUNT)) { kafkaHandler.sendEventToKafka(ProductResponseDto.from(newProduct)); + } } @Transactional @@ -104,8 +103,9 @@ public void updateProduct(Long productId, ProductRequestDto requestDto, List purchaseRequestDto, Member member) { + + public static KafkaPurchaseDto of(List purchaseRequestDto, Member member) { + return KafkaPurchaseDto.builder() + .purchaseRequestDto(purchaseRequestDto) + .member(member) + .build(); + } + +} diff --git a/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java b/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java index afdd3e1..eeb1be6 100644 --- a/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java @@ -1,21 +1,74 @@ package com.example.purebasketbe.global.config; +import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import com.example.purebasketbe.domain.purchase.dto.KafkaPurchaseDto; import org.apache.kafka.clients.admin.NewTopic; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.kafka.annotation.EnableKafka; import org.springframework.kafka.config.TopicBuilder; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; @EnableKafka @Configuration public class KafkaConfig { + @Value("${spring.kafka.bootstrap-servers}") + private String bootStrapServers; + @Bean - public NewTopic topic() { + public NewTopic saleEventTopic() { return TopicBuilder.name("event") .partitions(2) .replicas(2) .build(); } + @Bean + public NewTopic purchaseTopic() { + return TopicBuilder.name("purchase") + .partitions(2) +// .replicas(1) + .build(); + } + + @Bean + public Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootStrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(ProducerConfig.ACKS_CONFIG, "all"); + + return props; + } + + @Bean + public ProducerFactory eventProducerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs()); + } + @Bean + public ProducerFactory purchaseProducerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs()); + } + + @Bean + public KafkaTemplate eventKafkaTemplate() { + return new KafkaTemplate<>(eventProducerFactory()); + } + + @Bean + public KafkaTemplate purchaseKafkaTemplate() { + return new KafkaTemplate<>(purchaseProducerFactory()); + } + } diff --git a/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java b/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java index e437501..acb2c51 100644 --- a/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java +++ b/src/main/java/com/example/purebasketbe/global/kafka/KafkaService.java @@ -1,27 +1,34 @@ package com.example.purebasketbe.global.kafka; +import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.dto.ProductResponseDto; +import com.example.purebasketbe.domain.purchase.dto.KafkaPurchaseDto; +import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; +import java.util.List; + @Slf4j @Component @RequiredArgsConstructor public class KafkaService { - private final KafkaTemplate kafkaTemplate; + private final KafkaTemplate eventKafkaTemplate; + private final KafkaTemplate purchaseKafkaTemplate; - public static final String TOPIC_EVENT = "sale_event"; + public static final String TOPIC_EVENT = "event"; + public static final String TOPIC_PURCHASE = "purchase"; // Methods for Producer public void sendEventToKafka(ProductResponseDto responseDto) { - kafkaTemplate.send(TOPIC_EVENT, responseDto); + eventKafkaTemplate.send(TOPIC_EVENT, responseDto); } - // Consumers - // MemberService - sendEmailToMembers() - // SseService - alarmNewEvent() - + public void sendPurchaseToKafka(List purchaseRequestDto, Member member) { + KafkaPurchaseDto data = KafkaPurchaseDto.of(purchaseRequestDto, member); + purchaseKafkaTemplate.send( TOPIC_PURCHASE, data); + } } diff --git a/src/main/java/com/example/purebasketbe/global/sse/SseService.java b/src/main/java/com/example/purebasketbe/global/sse/SseService.java index 4e0165f..cfba6a1 100644 --- a/src/main/java/com/example/purebasketbe/global/sse/SseService.java +++ b/src/main/java/com/example/purebasketbe/global/sse/SseService.java @@ -37,31 +37,6 @@ public SseEmitter subscribe(String email, String lastEventId) { return emitter; } - @KafkaListener(topics = TOPIC_EVENT, groupId = "${spring.kafka.consumer.group-id}") - private void alarmNewEvent(ProductResponseDto responseDto) { - Map connectionMap = sseRepository.findAllEmitters(); - int salePrice = responseDto.price() * responseDto.discountRate() / 100; - String message = String.format(""" - 새로운 할인 이벤트! - %s %d%% 할인!! 한정수량 단 %d개!!!""", - responseDto.name(), responseDto.discountRate(), responseDto.stock()); - - if (!connectionMap.isEmpty()) { - connectionMap.forEach((id, emitter) -> notify(emitter, id, message)); - } - } - - private void notify(SseEmitter emitter, String emitterId, Object data) { - try { - emitter.send(SseEmitter.event() - .id(emitterId) - .data(data)); // String Type만 가능함 - } catch (IOException e) { - log.error("notify 실패 : {}", e.getMessage()); - sseRepository.delete(emitterId); - } - } - @Scheduled(fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3분마다 heartbeat 메세지 전달. public void sendHeartbeat() { Map connectionMap = sseRepository.findAllEmitters(); @@ -77,4 +52,31 @@ public void sendHeartbeat() { }); } } + +// @KafkaListener(topics = TOPIC_EVENT, groupId = "${spring.kafka.consumer.group-id}") +// private void alarmNewEvent(ProductResponseDto responseDto) { +// Map connectionMap = sseRepository.findAllEmitters(); +//// int salePrice = responseDto.price() * responseDto.discountRate() / 100; +// String message = String.format(""" +// 새로운 할인 이벤트! +// %s %d%% 할인!! 한정수량 단 %d개!!!""", +// responseDto.name(), responseDto.discountRate(), responseDto.stock()); +// +// if (!connectionMap.isEmpty()) { +// connectionMap.forEach((id, emitter) -> notify(emitter, id, message)); +// } +// } +// +// private void notify(SseEmitter emitter, String emitterId, Object data) { +// try { +// emitter.send(SseEmitter.event() +// .id(emitterId) +// .data(data)); // String Type만 가능함 +// } catch (IOException e) { +// log.error("notify 실패 : {}", e.getMessage()); +// sseRepository.delete(emitterId); +// } +// } + + } diff --git a/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java b/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java deleted file mode 100644 index 0188739..0000000 --- a/src/main/java/com/example/purebasketbe/global/tool/EmailContents.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.purebasketbe.global.tool; - -import com.example.purebasketbe.domain.product.dto.ProductResponseDto; -import lombok.Builder; - -public record EmailContents( - String subject, - String text -) { - @Builder - public EmailContents { - } - - public static EmailContents from(ProductResponseDto responseDto) { - int salePrice = responseDto.price() * (100 - responseDto.discountRate()) / 100; - return EmailContents.builder() - .subject("(광고) Pure Basket의 새로운 할인 이벤트가 시작됩니다!") - .text(String.format(""" - %s 할인 이벤트! - 어디에도 없을 가격 %d원!! - 한정 수량 %d개 쏩니다!!!""", - responseDto.name(), salePrice, responseDto.stock()) - ).build(); - } -} From b063626dc79c5349ca743aca2d66e50d0741fca3 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 29 Dec 2023 16:00:36 +0900 Subject: [PATCH 54/82] =?UTF-8?q?Feat:=20=EC=A3=BC=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=97=90=20Pessimistic=20Lock=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductRepository.java | 7 +++- .../domain/product/PurchaseFacade.java | 32 ++++++++++++++++++ .../domain/purchase/PurchaseController.java | 4 ++- .../domain/purchase/PurchaseService.java | 33 ++++--------------- 4 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index 29fb1a5..4b52f92 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -2,9 +2,12 @@ import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Product; +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -25,5 +28,7 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet Optional findByIdAndDeleted(Long productId, boolean isDeleted); - List findByIdInAndDeleted(List requestIds, boolean isDeleted); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT p FROM Product p WHERE p.id IN :requestedProductsIds AND p.deleted = :isDeleted") + List findByIdInAndDeleted(List requestedProductsIds, boolean isDeleted); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java new file mode 100644 index 0000000..387a40b --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java @@ -0,0 +1,32 @@ +package com.example.purebasketbe.domain.product; + +import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.domain.purchase.PurchaseService; +import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; +import com.example.purebasketbe.global.kafka.KafkaService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PurchaseFacade { + + private final PurchaseService purchaseService; + private final KafkaService kafkaService; + + public void purchaseProducts(List purchaseRequestDto, Member member) { + int size = purchaseRequestDto.size(); + + List requestedProductsIds = purchaseRequestDto.stream() + .map(PurchaseDetail::productId).toList(); + purchaseService.processPurchase(purchaseRequestDto, requestedProductsIds, size); + + log.info("회원 {}: 상품 구매 요청 적재", member.getId()); + kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); + } + +} diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index a0db8d8..e554332 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -1,6 +1,7 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.domain.product.PurchaseFacade; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.global.tool.LoginAccount; @@ -17,11 +18,12 @@ public class PurchaseController { private final PurchaseService purchaseService; + private final PurchaseFacade purchaseFacade; @PostMapping public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseRequestDto requestDto, @LoginAccount Member member) { - purchaseService.purchaseProducts(requestDto.purchaseList(), member); + purchaseFacade.purchaseProducts(requestDto.purchaseList(), member); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 6a6f0a6..342f2a8 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -1,16 +1,15 @@ package com.example.purebasketbe.domain.purchase; -import com.example.purebasketbe.domain.cart.CartRepository; import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; -import com.example.purebasketbe.domain.recipe.dto.RecipeResponseDto; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -19,52 +18,34 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class PurchaseService { private final PurchaseRepository purchaseRepository; - private final CartRepository cartRepository; private final ProductRepository productRepository; private final int PRODUCTS_PER_PAGE = 10; @Transactional - public void purchaseProducts(final List purchaseRequestDto, Member member) { - int size = purchaseRequestDto.size(); + public void processPurchase(List purchaseRequestDto, List requestedProductsIds, int size) { + List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); + List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); - List sortedPurchaseDetailList = purchaseRequestDto.stream() - .sorted(Comparator.comparing(PurchaseDetail::productId)).toList(); - List requestIds = sortedPurchaseDetailList.stream() - .map(PurchaseDetail::productId).toList(); - - - List validProductList = productRepository.findByIdInAndDeleted(requestIds, false); validateProducts(size, validProductList); - List amountList = sortedPurchaseDetailList.stream() - .map(PurchaseDetail::amount).toList(); - - List purchaseList = new ArrayList<>(); for (int i = 0; i < size; i++) { Product product = validProductList.get(i); int amount = amountList.get(i); checkProductStock(product, amount); - - Purchase purchase = Purchase.of(product, amount, member); - purchaseList.add(purchase); - - product.incrementSalesCount(amount); product.decrementStock(amount); + product.incrementSalesCount(amount); } - - purchaseRepository.saveAll(purchaseList); - cartRepository.deleteByUserAndProductIn(member, validProductList); } + @Transactional(readOnly = true) public Page getPurchases(Member member, int page, String sortBy, String order) { Sort.Direction direction = Direction.valueOf(order.toUpperCase()); From 84a5793870170f2f35734cc7ad9a804f3e723bd8 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 29 Dec 2023 21:42:50 +0900 Subject: [PATCH 55/82] =?UTF-8?q?Rename:=20PurchaseFacade.java=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A5=BC=20Purchase=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/purchase/PurchaseController.java | 1 - .../domain/{product => purchase}/PurchaseFacade.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename src/main/java/com/example/purebasketbe/domain/{product => purchase}/PurchaseFacade.java (95%) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index e554332..d2a516f 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -1,7 +1,6 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.product.PurchaseFacade; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.global.tool.LoginAccount; diff --git a/src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java similarity index 95% rename from src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java rename to src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java index 387a40b..d60eb87 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/PurchaseFacade.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java @@ -1,4 +1,4 @@ -package com.example.purebasketbe.domain.product; +package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.purchase.PurchaseService; From b0a43c58cd78a886d809917c602c2a3f4fb93b52 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 29 Dec 2023 22:24:26 +0900 Subject: [PATCH 56/82] =?UTF-8?q?Update:=20=EA=B5=AC=EB=A7=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=A0=20=EC=A6=9D=EA=B0=80=20=EB=B0=8F=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=ED=9A=9F=EC=88=98=20=EC=A6=9D=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20batchUpdate=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/purchase/PurchaseService.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 342f2a8..655dee7 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -15,9 +15,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.util.List; @Slf4j @@ -27,6 +31,7 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; private final ProductRepository productRepository; + private final JdbcTemplate jdbcTemplate; private final int PRODUCTS_PER_PAGE = 10; @@ -40,9 +45,27 @@ public void processPurchase(List purchaseRequestDto, List Product product = validProductList.get(i); int amount = amountList.get(i); checkProductStock(product, amount); - product.decrementStock(amount); - product.incrementSalesCount(amount); } + productBatchUpdate(validProductList, amountList); + } + + public void productBatchUpdate(List productList, List amountList) { + String sql = "UPDATE product SET stock = stock - ?, sales_count = sales_count + ? WHERE id = ?"; + + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + ps.setLong(1, amountList.get(i)); + ps.setLong(2, amountList.get(i)); + ps.setLong(3, productList.get(i).getId()); + } + + @Override + public int getBatchSize() { + return productList.size(); + } + }); + } From 73cb8f65495b00540be89d2804ae1ad7cdb78014 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 30 Dec 2023 19:02:15 +0900 Subject: [PATCH 57/82] =?UTF-8?q?Feat:=20Stock,=20PurchaseDetail=20entity?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stock, purchase_detail 테이블 추가로 인한 코드 업데이트 --- .../domain/product/ProductRepository.java | 7 +-- .../domain/product/ProductService.java | 13 +++- .../product/dto/ProductResponseDto.java | 2 - .../domain/product/entity/Product.java | 24 ++----- .../domain/product/entity/Stock.java | 47 ++++++++++++++ .../domain/purchase/PurchaseRepository.java | 3 - .../domain/purchase/PurchaseService.java | 62 +++++++++---------- .../purchase/dto/PurchaseResponseDto.java | 16 +---- .../domain/purchase/entity/Purchase.java | 25 +++----- .../purchase/entity/PurchaseDetail.java | 29 +++++++++ 10 files changed, 134 insertions(+), 94 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java create mode 100644 src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index 4b52f92..76e41aa 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -2,12 +2,9 @@ import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Product; -import jakarta.persistence.LockModeType; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.List; @@ -28,7 +25,7 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet Optional findByIdAndDeleted(Long productId, boolean isDeleted); - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT p FROM Product p WHERE p.id IN :requestedProductsIds AND p.deleted = :isDeleted") +// @Query("SELECT p FROM Product p WHERE p.id IN :requestedProductsIds AND p.deleted = :isDeleted") List findByIdInAndDeleted(List requestedProductsIds, boolean isDeleted); + } diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 48b765a..8b0b315 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -6,6 +6,7 @@ import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Image; import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.kafka.KafkaService; @@ -29,6 +30,7 @@ public class ProductService { private final ProductRepository productRepository; private final ImageRepository imageRepository; + private final StockRepository stockRepository; private final S3Handler s3Handler; private final KafkaService kafkaHandler; @@ -87,8 +89,10 @@ public ProductResponseDto getProduct(Long productId) { public void registerProduct(ProductRequestDto requestDto, List files) { checkExistProductByName(requestDto.name()); Product newProduct = Product.from(requestDto); + Stock stock = Stock.of(requestDto, newProduct); productRepository.save(newProduct); + stockRepository.save(stock); saveAndUploadImage(newProduct, files); if (newProduct.getEvent().equals(Event.DISCOUNT)) { @@ -99,9 +103,16 @@ public void registerProduct(ProductRequestDto requestDto, List fi @Transactional public void updateProduct(Long productId, ProductRequestDto requestDto, List files) { Product product = findProduct(productId); + Stock stock = product.getStock(); product.update(requestDto); - if (!files.isEmpty()) saveAndUploadImage(product, files); + if (requestDto.stock() != null) { + stock.update(requestDto.stock()); + } + + if (!files.isEmpty()) { + saveAndUploadImage(product, files); + } if (product.getEvent().equals(Event.DISCOUNT)) { kafkaHandler.sendEventToKafka(ProductResponseDto.from(product)); diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java index 52c4b0d..9ba0c98 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java @@ -29,7 +29,6 @@ public static ProductResponseDto of(Product product, List imgUrlList) { .id(product.getId()) .name(product.getName()) .price(product.getPrice()) - .stock(product.getStock()) .info(product.getInfo()) .category(product.getCategory()) .event(product.getEvent()) @@ -43,7 +42,6 @@ public static ProductResponseDto from(Product product) { .id(product.getId()) .name(product.getName()) .price(product.getPrice()) - .stock(product.getStock()) .info(product.getInfo()) .category(product.getCategory()) .event(product.getEvent()) diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index 373b8c2..3f09998 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -31,9 +31,6 @@ public class Product { @Column(nullable = false) private int price; - @Column(nullable = false) - private int stock; - private String info; private String category; @@ -43,8 +40,6 @@ public class Product { private int discountRate; - private int salesCount; - @CreatedDate private LocalDateTime createdAt; @@ -54,31 +49,32 @@ public class Product { @Column(nullable = false) private boolean deleted; + @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + private Stock stock; + @BatchSize(size = 21) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) private List images = new ArrayList<>(); @Builder - private Product(String name, Integer price, Integer stock, String info, - String category, Event event, Integer discountRate) { + private Product(String name, Integer price, String info, + String category, Event event, Integer discountRate, Stock stock) { this.name = name; this.price = price; - this.stock = stock; this.info = info; this.category = category; this.event = event; this.discountRate = discountRate; - this.salesCount = 0; this.createdAt = LocalDateTime.now(); this.modifiedAt = LocalDateTime.now(); this.deleted = false; + this.stock = stock; } public static Product from(ProductRequestDto requestDto) { return Product.builder() .name(requestDto.name()) .price(requestDto.price()) - .stock(requestDto.stock()) .info(requestDto.info()) .category(requestDto.category()) .event(requestDto.event()) @@ -89,7 +85,6 @@ public static Product from(ProductRequestDto requestDto) { public void update(ProductRequestDto requestDto) { this.name = requestDto.name() == null ? this.name : requestDto.name(); this.price = requestDto.price() == null ? this.price : requestDto.price(); - this.stock = requestDto.stock() == null ? this.stock : this.stock + requestDto.stock(); this.info = requestDto.info() == null ? this.info : requestDto.info(); this.category = requestDto.category() == null ? this.category : requestDto.category(); this.event = requestDto.event() == null ? this.event : requestDto.event(); @@ -103,11 +98,4 @@ public void softDelete() { this.deleted = true; } - public void incrementSalesCount(int amount) { - this.salesCount += amount; - } - - public void decrementStock(int amount) { - this.stock -= amount; - } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java new file mode 100644 index 0000000..d79bc2e --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java @@ -0,0 +1,47 @@ +package com.example.purebasketbe.domain.product.entity; + +import com.example.purebasketbe.domain.product.dto.ProductRequestDto; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "stock") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Stock { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private int stock; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Builder + private Stock(int stock, Product product) { + this.stock = stock; + this.product = product; + } + + public static Stock of(ProductRequestDto requestDto, Product product) { + return Stock.builder() + .stock(requestDto.stock()) + .product(product) + .build(); + } + + public void update(int stock) { + this.stock = stock; + } + + public void decrementStock(int amount) { + this.stock -= amount; + } + +} diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseRepository.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseRepository.java index 1beda7e..8adc380 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseRepository.java @@ -2,17 +2,14 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.purchase.entity.Purchase; -import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface PurchaseRepository extends JpaRepository { - @Query("SELECT p FROM Purchase p JOIN FETCH p.product WHERE p.member = :member") Page findAllByMember(Member member, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 655dee7..ca896fe 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -3,6 +3,7 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; @@ -15,13 +16,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; -import org.springframework.jdbc.core.BatchPreparedStatementSetter; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.sql.PreparedStatement; -import java.sql.SQLException; import java.util.List; @Slf4j @@ -31,43 +28,43 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; private final ProductRepository productRepository; - private final JdbcTemplate jdbcTemplate; private final int PRODUCTS_PER_PAGE = 10; @Transactional public void processPurchase(List purchaseRequestDto, List requestedProductsIds, int size) { List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); + validateProducts(size, validProductList); + + List stockList = validProductList.stream().map(Product::getStock).toList(); List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); - validateProducts(size, validProductList); for (int i = 0; i < size; i++) { - Product product = validProductList.get(i); + Stock stock = stockList.get(i); int amount = amountList.get(i); - checkProductStock(product, amount); + checkProductStock(stock, amount); + stock.decrementStock(amount); } - productBatchUpdate(validProductList, amountList); - } - - public void productBatchUpdate(List productList, List amountList) { - String sql = "UPDATE product SET stock = stock - ?, sales_count = sales_count + ? WHERE id = ?"; - - jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - ps.setLong(1, amountList.get(i)); - ps.setLong(2, amountList.get(i)); - ps.setLong(3, productList.get(i).getId()); - } - - @Override - public int getBatchSize() { - return productList.size(); - } - }); - +// productBatchUpdate(updatedProductList, amountList); } +// public void productBatchUpdate(List productList, List amountList) { +// String sql = "UPDATE product SET stock = stock - ?, sales_count = sales_count + ? WHERE id = ?"; +// +// jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { +// @Override +// public void setValues(PreparedStatement ps, int i) throws SQLException { +// ps.setLong(1, amountList.get(i)); +// ps.setLong(2, amountList.get(i)); +// ps.setLong(3, productList.get(i).getId()); +// } +// @Override +// public int getBatchSize() { +// return productList.size(); +// } +// }); +// +// } @Transactional(readOnly = true) public Page getPurchases(Member member, int page, String sortBy, String order) { @@ -77,10 +74,7 @@ public Page getPurchases(Member member, int page, String so Page purchases = purchaseRepository.findAllByMember(member, pageable); - return purchases.map(purchase -> { - Product product = getProductById(purchase.getProduct().getId()); - return PurchaseResponseDto.of(product, purchase); - }); + return purchases.map(PurchaseResponseDto::from); } private static void validateProducts(int size, List validProductList) { @@ -89,8 +83,8 @@ private static void validateProducts(int size, List validProductList) { } } - private static void checkProductStock(Product product, int amount) { - if (product.getStock() < amount) { + private static void checkProductStock(Stock stock, int amount) { + if (stock.getStock() < amount) { throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); } } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java index 80cdb09..facc5ef 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java @@ -1,19 +1,13 @@ package com.example.purebasketbe.domain.purchase.dto; -import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.purchase.entity.Purchase; -import lombok.AccessLevel; import lombok.Builder; -import lombok.RequiredArgsConstructor; import java.time.LocalDateTime; public record PurchaseResponseDto( - Long productId, - String name, - int amount, - int price, + Long id, int totalPrice, LocalDateTime purchasedAt ) { @@ -21,13 +15,9 @@ public record PurchaseResponseDto( public PurchaseResponseDto { } - public static PurchaseResponseDto of(Product product, Purchase purchase) { + public static PurchaseResponseDto from(Purchase purchase) { return PurchaseResponseDto.builder() - .productId(product.getId()) - .name(product.getName()) - .amount(purchase.getAmount()) - .price(purchase.getPrice()) - .totalPrice(purchase.getPrice() * purchase.getAmount()) + .totalPrice(purchase.getTotalPrice()) .purchasedAt(purchase.getPurchasedAt()) .build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java index 0baf6b0..1209b68 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java @@ -18,35 +18,24 @@ public class Purchase extends TimeStamp{ @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Min(value = 1) - @Column(nullable = false) - private int amount; - - @Column(nullable = false) - private int price; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "product_id") - private Product product; + @Column(nullable = false) + private int totalPrice; @Builder - private Purchase(int amount, int price, Member member, Product product) { - this.amount = amount; - this.price = price; + private Purchase(Member member, int totalPrice) { + this.member = member; - this.product = product; + this.totalPrice = totalPrice; } - public static Purchase of(Product product, int amount, Member member) { + public static Purchase of(Member member, int totalPrice) { return Purchase.builder() - .amount(amount) - .price(product.getPrice()) .member(member) - .product(product) + .totalPrice(totalPrice) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java new file mode 100644 index 0000000..3818d38 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java @@ -0,0 +1,29 @@ +package com.example.purebasketbe.domain.purchase.entity; + +import com.example.purebasketbe.domain.product.entity.Product; +import jakarta.persistence.*; +import jakarta.validation.constraints.Min; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "purchase_detail") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PurchaseDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Min(value = 1) + @Column(nullable = false) + private int amount; + + @Column(nullable = false) + private int price; +} From 66077458b632c24002be33113bcdb091d06415b9 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 30 Dec 2023 20:13:27 +0900 Subject: [PATCH 58/82] =?UTF-8?q?Fix:=20Product,=20Purchase=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductRepository.java | 1 - .../domain/product/StockRepository.java | 16 ++++++++++++++++ .../domain/product/dto/ProductResponseDto.java | 1 - .../purchase/PurchaseDetailRepository.java | 10 ++++++++++ .../domain/purchase/PurchaseFacade.java | 3 +-- .../domain/purchase/PurchaseService.java | 7 +++++-- 6 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/StockRepository.java create mode 100644 src/main/java/com/example/purebasketbe/domain/purchase/PurchaseDetailRepository.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index 76e41aa..e3724e5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -25,7 +25,6 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet Optional findByIdAndDeleted(Long productId, boolean isDeleted); -// @Query("SELECT p FROM Product p WHERE p.id IN :requestedProductsIds AND p.deleted = :isDeleted") List findByIdInAndDeleted(List requestedProductsIds, boolean isDeleted); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java new file mode 100644 index 0000000..1c76c09 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -0,0 +1,16 @@ +package com.example.purebasketbe.domain.product; + +import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.product.entity.Stock; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface StockRepository extends JpaRepository { + + @Query("SELECT s FROM Stock s WHERE s.product IN (:productList)") + List findAllByProductIn(List productList); +} diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java index 9ba0c98..96e072f 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java @@ -12,7 +12,6 @@ public record ProductResponseDto( Long id, String name, int price, - int stock, String info, String category, Event event, diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseDetailRepository.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseDetailRepository.java new file mode 100644 index 0000000..a6208ae --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseDetailRepository.java @@ -0,0 +1,10 @@ +package com.example.purebasketbe.domain.purchase; + +import com.example.purebasketbe.domain.purchase.entity.PurchaseDetail; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface PurchaseDetailRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java index d60eb87..b38e755 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java @@ -19,11 +19,10 @@ public class PurchaseFacade { private final KafkaService kafkaService; public void purchaseProducts(List purchaseRequestDto, Member member) { - int size = purchaseRequestDto.size(); List requestedProductsIds = purchaseRequestDto.stream() .map(PurchaseDetail::productId).toList(); - purchaseService.processPurchase(purchaseRequestDto, requestedProductsIds, size); + purchaseService.processPurchase(purchaseRequestDto, requestedProductsIds); log.info("회원 {}: 상품 구매 요청 적재", member.getId()); kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index ca896fe..56bec57 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -2,6 +2,7 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.ProductRepository; +import com.example.purebasketbe.domain.product.StockRepository; import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; @@ -28,15 +29,17 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; private final ProductRepository productRepository; + private final StockRepository stockRepository; private final int PRODUCTS_PER_PAGE = 10; @Transactional - public void processPurchase(List purchaseRequestDto, List requestedProductsIds, int size) { + public void processPurchase(List purchaseRequestDto, List requestedProductsIds) { + int size = purchaseRequestDto.size(); List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); validateProducts(size, validProductList); - List stockList = validProductList.stream().map(Product::getStock).toList(); + List stockList = stockRepository.findAllByProductIn(validProductList); List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); for (int i = 0; i < size; i++) { From 9da35dea88a9953f1f334a215a2ffee61c574654 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sun, 31 Dec 2023 22:40:07 +0900 Subject: [PATCH 59/82] =?UTF-8?q?Update:=20Product=20entity=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/product/ProductRepository.java | 1 - .../example/purebasketbe/domain/product/entity/Product.java | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java index e3724e5..687750a 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductRepository.java @@ -22,7 +22,6 @@ Page findAllByDeletedAndEventAndCategoryAndNameContains(boolean isDelet Page findAllByDeletedAndEventAndNameContains(boolean isDeleted, Event event, String query, Pageable pageable); - Optional findByIdAndDeleted(Long productId, boolean isDeleted); List findByIdInAndDeleted(List requestedProductsIds, boolean isDeleted); diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index 3f09998..7701214 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -49,11 +49,11 @@ public class Product { @Column(nullable = false) private boolean deleted; - @OneToOne(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) private Stock stock; @BatchSize(size = 21) - @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) private List images = new ArrayList<>(); @Builder @@ -97,5 +97,4 @@ public void softDelete() { this.modifiedAt = LocalDateTime.now(); this.deleted = true; } - } \ No newline at end of file From 1ca8994f5c458f391877d71ed56f63d5689758b1 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sun, 31 Dec 2023 22:41:18 +0900 Subject: [PATCH 60/82] =?UTF-8?q?Update:=20Purchase=EC=99=80=20PurchaseDet?= =?UTF-8?q?ail=20entity=EC=9D=98=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/purchase/entity/Purchase.java | 23 +++++++++++-------- .../purchase/entity/PurchaseDetail.java | 4 ++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java index 1209b68..ec3e1d2 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java @@ -1,14 +1,15 @@ package com.example.purebasketbe.domain.purchase.entity; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.product.entity.Product; import jakarta.persistence.*; -import jakarta.validation.constraints.Min; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @Table(name = "purchase") @@ -22,6 +23,10 @@ public class Purchase extends TimeStamp{ @JoinColumn(name = "member_id") private Member member; + @OneToMany(mappedBy="purchase", fetch = FetchType.LAZY, + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) + private List purchaseDetails = new ArrayList<>(); + @Column(nullable = false) private int totalPrice; @@ -31,11 +36,11 @@ private Purchase(Member member, int totalPrice) { this.member = member; this.totalPrice = totalPrice; } - - public static Purchase of(Member member, int totalPrice) { - return Purchase.builder() - .member(member) - .totalPrice(totalPrice) - .build(); - } +// +// public static Purchase of(Member member, int totalPrice) { +// return Purchase.builder() +// .member(member) +// .totalPrice(totalPrice) +// .build(); +// } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java index 3818d38..8451a44 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java @@ -20,6 +20,10 @@ public class PurchaseDetail { @JoinColumn(name = "product_id") private Product product; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "purchase_id") + private Purchase purchase; + @Min(value = 1) @Column(nullable = false) private int amount; From adce47d2660b4b769271a41b5b462b35a24549a1 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sun, 31 Dec 2023 22:42:50 +0900 Subject: [PATCH 61/82] =?UTF-8?q?Update:=20=EC=A3=BC=EB=AC=B8=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9E=AC=EA=B3=A0=20=EC=B0=A8=EA=B0=90=ED=95=A0=20=EB=95=8C?= =?UTF-8?q?=20stock=20table=EC=97=90=EB=A7=8C=20=EB=9D=BD=EC=9D=84=20?= =?UTF-8?q?=EA=B1=B8=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/StockRepository.java | 9 +++-- .../domain/purchase/PurchaseFacade.java | 19 +++++++-- .../domain/purchase/PurchaseService.java | 39 ++----------------- 3 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index 1c76c09..8472285 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -1,8 +1,9 @@ package com.example.purebasketbe.domain.product; -import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -11,6 +12,8 @@ @Repository public interface StockRepository extends JpaRepository { - @Query("SELECT s FROM Stock s WHERE s.product IN (:productList)") - List findAllByProductIn(List productList); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Stock s WHERE s.product.id IN :requestedProductsIds") + List findAllByProductIdIn(List requestedProductsIds); + } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java index b38e755..48deeed 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java @@ -1,8 +1,11 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.purchase.PurchaseService; +import com.example.purebasketbe.domain.product.ProductRepository; +import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; +import com.example.purebasketbe.global.exception.CustomException; +import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.kafka.KafkaService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,15 +20,25 @@ public class PurchaseFacade { private final PurchaseService purchaseService; private final KafkaService kafkaService; + private final ProductRepository productRepository; public void purchaseProducts(List purchaseRequestDto, Member member) { + int size = purchaseRequestDto.size(); List requestedProductsIds = purchaseRequestDto.stream() .map(PurchaseDetail::productId).toList(); - purchaseService.processPurchase(purchaseRequestDto, requestedProductsIds); + List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); + validateProducts(size, validProductList); - log.info("회원 {}: 상품 구매 요청 적재", member.getId()); + purchaseService.processPurchase(purchaseRequestDto, size, requestedProductsIds); kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); + log.info("회원 {}: 상품 구매 요청 적재", member.getId()); + } + + private static void validateProducts(int size, List validProductList) { + if (size != validProductList.size()) { + throw new CustomException(ErrorCode.PRODUCT_NOT_FOUND); + } } } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 56bec57..0a9fc1a 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -1,9 +1,7 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.StockRepository; -import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; @@ -28,18 +26,14 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; - private final ProductRepository productRepository; private final StockRepository stockRepository; private final int PRODUCTS_PER_PAGE = 10; @Transactional - public void processPurchase(List purchaseRequestDto, List requestedProductsIds) { - int size = purchaseRequestDto.size(); - List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); - validateProducts(size, validProductList); + public void processPurchase(List purchaseRequestDto, int size, List requestedProductsIds) { - List stockList = stockRepository.findAllByProductIn(validProductList); + List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); for (int i = 0; i < size; i++) { @@ -48,26 +42,9 @@ public void processPurchase(List purchaseRequestDto, List checkProductStock(stock, amount); stock.decrementStock(amount); } -// productBatchUpdate(updatedProductList, amountList); + } -// public void productBatchUpdate(List productList, List amountList) { -// String sql = "UPDATE product SET stock = stock - ?, sales_count = sales_count + ? WHERE id = ?"; -// -// jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { -// @Override -// public void setValues(PreparedStatement ps, int i) throws SQLException { -// ps.setLong(1, amountList.get(i)); -// ps.setLong(2, amountList.get(i)); -// ps.setLong(3, productList.get(i).getId()); -// } -// @Override -// public int getBatchSize() { -// return productList.size(); -// } -// }); -// -// } @Transactional(readOnly = true) public Page getPurchases(Member member, int page, String sortBy, String order) { @@ -80,11 +57,6 @@ public Page getPurchases(Member member, int page, String so return purchases.map(PurchaseResponseDto::from); } - private static void validateProducts(int size, List validProductList) { - if (size != validProductList.size()) { - throw new CustomException(ErrorCode.PRODUCT_NOT_FOUND); - } - } private static void checkProductStock(Stock stock, int amount) { if (stock.getStock() < amount) { @@ -92,9 +64,4 @@ private static void checkProductStock(Stock stock, int amount) { } } - private Product getProductById(Long id) { - return productRepository.findById(id).orElseThrow( - () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) - ); - } } \ No newline at end of file From c3677e6cdcabbca5f93a50285a3367404aa0be41 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 1 Jan 2024 00:15:50 +0900 Subject: [PATCH 62/82] =?UTF-8?q?Update:=20CartRepository=20deleteAllByMem?= =?UTF-8?q?berAndProductIn=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/domain/cart/CartRepository.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index 4b27f78..740a93f 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -17,15 +17,13 @@ public interface CartRepository extends JpaRepository { @Query("select exists (select c.id from Cart c where c.product = :product)") boolean existsProduct(Product product); - @Modifying + @Modifying(clearAutomatically = true) @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") - void deleteByMemberAndProductIn(Member member, List products); + void deleteAllByMemberAndProductIn(Member member, List products); @Query("SELECT c FROM Cart c JOIN FETCH c.product WHERE c.member = :member") List findAllByMember(Member member); - void deleteAllByMemberAndProductIn(Member member, List productList); - Optional findByProductIdAndMember(Long productId, Member member); } \ No newline at end of file From 5fd1370382b635bc2e9833ea07dc1f17aa4fe080 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 1 Jan 2024 19:24:30 +0900 Subject: [PATCH 63/82] =?UTF-8?q?Update:=20=EB=9D=BD=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Facade class 없애고 PurchaseService class에서 하나의 메서드로 구현해서, 불필요한 query가 생성되지 않도록 함. --- .../domain/product/ProductService.java | 1 + .../domain/product/StockRepository.java | 1 + .../domain/product/entity/Product.java | 4 ++ .../domain/purchase/PurchaseController.java | 3 +- .../domain/purchase/PurchaseFacade.java | 44 ------------------- .../domain/purchase/PurchaseService.java | 22 ++++++++-- 6 files changed, 26 insertions(+), 49 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 8b0b315..16c8d55 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -90,6 +90,7 @@ public void registerProduct(ProductRequestDto requestDto, List fi checkExistProductByName(requestDto.name()); Product newProduct = Product.from(requestDto); Stock stock = Stock.of(requestDto, newProduct); + newProduct.attachStock(stock); productRepository.save(newProduct); stockRepository.save(stock); diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index 8472285..fcbe790 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -1,5 +1,6 @@ package com.example.purebasketbe.domain.product; +import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index 7701214..1452fc5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -97,4 +97,8 @@ public void softDelete() { this.modifiedAt = LocalDateTime.now(); this.deleted = true; } + + public void attachStock(Stock stock) { + this.stock = stock; + } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index d2a516f..a0db8d8 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -17,12 +17,11 @@ public class PurchaseController { private final PurchaseService purchaseService; - private final PurchaseFacade purchaseFacade; @PostMapping public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseRequestDto requestDto, @LoginAccount Member member) { - purchaseFacade.purchaseProducts(requestDto.purchaseList(), member); + purchaseService.purchaseProducts(requestDto.purchaseList(), member); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java deleted file mode 100644 index 48deeed..0000000 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.purebasketbe.domain.purchase; - -import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.product.ProductRepository; -import com.example.purebasketbe.domain.product.entity.Product; -import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; -import com.example.purebasketbe.global.exception.CustomException; -import com.example.purebasketbe.global.exception.ErrorCode; -import com.example.purebasketbe.global.kafka.KafkaService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PurchaseFacade { - - private final PurchaseService purchaseService; - private final KafkaService kafkaService; - private final ProductRepository productRepository; - - public void purchaseProducts(List purchaseRequestDto, Member member) { - int size = purchaseRequestDto.size(); - - List requestedProductsIds = purchaseRequestDto.stream() - .map(PurchaseDetail::productId).toList(); - List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); - validateProducts(size, validProductList); - - purchaseService.processPurchase(purchaseRequestDto, size, requestedProductsIds); - kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); - log.info("회원 {}: 상품 구매 요청 적재", member.getId()); - } - - private static void validateProducts(int size, List validProductList) { - if (size != validProductList.size()) { - throw new CustomException(ErrorCode.PRODUCT_NOT_FOUND); - } - } - -} diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 0a9fc1a..d2465b5 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -2,12 +2,14 @@ import com.example.purebasketbe.domain.member.entity.Member; import com.example.purebasketbe.domain.product.StockRepository; +import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.kafka.KafkaService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -27,15 +29,22 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; private final StockRepository stockRepository; + private final KafkaService kafkaService; private final int PRODUCTS_PER_PAGE = 10; @Transactional - public void processPurchase(List purchaseRequestDto, int size, List requestedProductsIds) { - + public void purchaseProducts(List purchaseRequestDto, Member member) { + int size = purchaseRequestDto.size(); + // Lock 적용 + List requestedProductsIds = purchaseRequestDto.stream() + .map(PurchaseDetail::productId).toList(); List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); - List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); + List validProductList = stockList.stream().map(Stock::getProduct).toList(); + validateProducts(size, validProductList); + + List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); for (int i = 0; i < size; i++) { Stock stock = stockList.get(i); int amount = amountList.get(i); @@ -43,6 +52,8 @@ public void processPurchase(List purchaseRequestDto, int size, L stock.decrementStock(amount); } + kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); + log.info("회원 {}: 상품 구매 요청 적재", member.getId()); } @@ -57,6 +68,11 @@ public Page getPurchases(Member member, int page, String so return purchases.map(PurchaseResponseDto::from); } + private static void validateProducts(int size, List validProductList) { + if (size != validProductList.size()) { + throw new CustomException(ErrorCode.PRODUCT_NOT_FOUND); + } + } private static void checkProductStock(Stock stock, int amount) { if (stock.getStock() < amount) { From 32f4bdd8fe28dd08a77ff257f2c5041da8e98482 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 25 Dec 2023 13:40:17 +0900 Subject: [PATCH 64/82] =?UTF-8?q?Feat:=20Redis=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductService.java | 4 ++ .../global/config/RedisCacheConfig.java | 66 +++++++++++++++++++ .../global/config/RedisConfig.java | 14 ++-- 3 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 8b0b315..24564c1 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -12,7 +12,9 @@ import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -26,6 +28,7 @@ @Service @RequiredArgsConstructor +@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; @@ -39,6 +42,7 @@ public class ProductService { @Value("${products.page.size}") private int pageSize; + @Cacheable(value = "products", key = "#eventPage + '_' + #page") @Transactional(readOnly = true) public ProductListResponseDto getProducts(int eventPage, int page) { Pageable eventPageable = getPageable(eventPage, eventPageSize); diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java new file mode 100644 index 0000000..31a95b3 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java @@ -0,0 +1,66 @@ +package com.example.purebasketbe.global.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +public class RedisCacheConfig { + @Value("${spring.cache.redis.host}") + private String cacheHost; + + @Value("${spring.cache.redis.port}") + private int cachePort; + + @Bean(name = "redisCacheConnectionFactory") + public RedisConnectionFactory redisCacheConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(cacheHost); + redisStandaloneConfiguration.setPort(cachePort); + return new LettuceConnectionFactory(redisStandaloneConfiguration); + } + + @Bean(name = "redisCacheTemplate") + public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } + + @Bean + public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { + RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()).build(); + return redisCacheManager; + } + + private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { + return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(300)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java index e53b288..494e676 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -1,10 +1,13 @@ package com.example.purebasketbe.global.config; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -14,12 +17,15 @@ @Configuration @EnableRedisRepositories public class RedisConfig { - private final RedisProperties redisProperties; + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private int port; - // RedisProperties로 yaml에 저장한 host, post를 연결 @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + @Primary + public LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); } // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 From fb718b213e05e2073347d180dacdcdde705faa42 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Mon, 25 Dec 2023 22:18:17 +0900 Subject: [PATCH 65/82] =?UTF-8?q?Fix:=20Redis=20cache=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=20=EC=8B=9C=20SerializationException=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/PureBasketBeApplication.java | 2 ++ .../domain/product/ProductService.java | 10 +++---- .../purebasketbe/global/RestPageImpl.java | 29 +++++++++++++++++++ .../global/config/RedisCacheConfig.java | 7 ++--- 4 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/RestPageImpl.java diff --git a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java index 7a1963d..bb7c46e 100644 --- a/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java +++ b/src/main/java/com/example/purebasketbe/PureBasketBeApplication.java @@ -2,9 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableCaching @EnableScheduling @EnableJpaAuditing @SpringBootApplication diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 24564c1..12a4157 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -7,12 +7,12 @@ import com.example.purebasketbe.domain.product.entity.Image; import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; +import com.example.purebasketbe.global.RestPageImpl; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; @@ -28,7 +28,6 @@ @Service @RequiredArgsConstructor -@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; @@ -48,13 +47,14 @@ public ProductListResponseDto getProducts(int eventPage, int page) { Pageable eventPageable = getPageable(eventPage, eventPageSize); Pageable pageable = getPageable(page, pageSize); + Page eventProducts = productRepository.findAllByDeletedAndEvent(false, Event.DISCOUNT, eventPageable); Page products = productRepository.findAllByDeletedAndEvent(false, Event.NORMAL, pageable); - Page eventProductsResponse = getResponseDtoFromProducts(eventProducts); - Page productsResponse = getResponseDtoFromProducts(products); + RestPageImpl eventProductsRestPage = RestPageImpl.from(getResponseDtoFromProducts(eventProducts)); + RestPageImpl productsRestPage = RestPageImpl.from(getResponseDtoFromProducts(products)); - return ProductListResponseDto.of(eventProductsResponse, productsResponse); + return ProductListResponseDto.of(eventProductsRestPage, productsRestPage); } @Transactional(readOnly = true) diff --git a/src/main/java/com/example/purebasketbe/global/RestPageImpl.java b/src/main/java/com/example/purebasketbe/global/RestPageImpl.java new file mode 100644 index 0000000..5bc497f --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/RestPageImpl.java @@ -0,0 +1,29 @@ +package com.example.purebasketbe.global; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"}) +public class RestPageImpl extends PageImpl { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private RestPageImpl(@JsonProperty("content") List content, + @JsonProperty("number") int number, + @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + private RestPageImpl(Page page) { + super(page.getContent(), page.getPageable(), page.getTotalElements()); + } + + public static RestPageImpl from(Page page) { + return new RestPageImpl(page); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java index 31a95b3..97e5e41 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java @@ -3,9 +3,9 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; @@ -17,7 +17,6 @@ import java.time.Duration; -@EnableCaching @Configuration public class RedisCacheConfig { @Value("${spring.cache.redis.host}") @@ -52,8 +51,8 @@ public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") return redisCacheManager; } - private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { - return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() + private RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(300)) .disableCachingNullValues() .serializeKeysWith( From 0b94e0774009a0d2e9c0eb457ce330c2cfc57a98 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 3 Jan 2024 00:42:58 +0900 Subject: [PATCH 66/82] =?UTF-8?q?Update:=20=EB=A0=88=EB=94=94=EC=8A=A4=20s?= =?UTF-8?q?entinel=20=EA=B5=AC=EC=84=B1=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RedisCacheConfig.java | 65 -------------- .../global/config/RedisConfig.java | 88 +++++++++++++++++-- 2 files changed, 82 insertions(+), 71 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java deleted file mode 100644 index 97e5e41..0000000 --- a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.example.purebasketbe.global.config; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.CacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -@Configuration -public class RedisCacheConfig { - @Value("${spring.cache.redis.host}") - private String cacheHost; - - @Value("${spring.cache.redis.port}") - private int cachePort; - - @Bean(name = "redisCacheConnectionFactory") - public RedisConnectionFactory redisCacheConnectionFactory() { - RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); - redisStandaloneConfiguration.setHostName(cacheHost); - redisStandaloneConfiguration.setPort(cachePort); - return new LettuceConnectionFactory(redisStandaloneConfiguration); - } - - @Bean(name = "redisCacheTemplate") - public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(connectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - - return redisTemplate; - } - - @Bean - public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { - RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration()).build(); - return redisCacheManager; - } - - private RedisCacheConfiguration redisCacheConfiguration() { - return RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofSeconds(300)) - .disableCachingNullValues() - .serializeKeysWith( - RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) - ) - .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java index 494e676..937fed8 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -1,31 +1,62 @@ package com.example.purebasketbe.global.config; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.PropertySource; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; +import java.time.Duration; + @RequiredArgsConstructor @Configuration @EnableRedisRepositories public class RedisConfig { - @Value("${spring.data.redis.host}") - private String host; - @Value("${spring.data.redis.port}") + @Value("${spring.data.redis.sentinel.master}") + private String sentinel1; + @Value("${spring.data.redis.sentinel.replica1}") + private String sentinel2; + @Value("${spring.data.redis.sentinel.replica2}") + private String sentinel3; + @Value("${spring.data.redis.sentinel.port}") private int port; + // local에서만 필요한 포트 + @Value("${spring.data.redis.sentinel.port2}") + private int port2; + @Value("${spring.data.redis.sentinel.port3}") + private int port3; + + @Value("${spring.data.redis.password}") + private String password; + + @Bean - @Primary - public LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); + public RedisConnectionFactory redisConnectionFactory() { + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() + .master("mymaster") + .sentinel(sentinel1, port) + .sentinel(sentinel2, port2) + .sentinel(sentinel3, port3); + + sentinelConfig.setPassword(password); + return new LettuceConnectionFactory(sentinelConfig ); + } // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 @@ -38,4 +69,49 @@ public RedisTemplate redisTemplate() { return redisTemplate; } + + @Bean + public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()).build(); + return redisCacheManager; + } + + private RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(24)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } + +// @Bean +// @Primary +// public LettuceConnectionFactory redisConnectionFactory() { +// return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); +// } + +// @Bean(name = "redisCacheConnectionFactory") +// public RedisConnectionFactory redisCacheConnectionFactory() { +// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); +// redisStandaloneConfiguration.setHostName(cacheHost); +// redisStandaloneConfiguration.setPort(cachePort); +// return new LettuceConnectionFactory(redisStandaloneConfiguration); +// } + +// @Bean(name = "redisCacheTemplate") +// public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { +// RedisTemplate redisTemplate = new RedisTemplate<>(); +// redisTemplate.setConnectionFactory(connectionFactory); +// redisTemplate.setKeySerializer(new StringRedisSerializer()); +// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); +// +// return redisTemplate; +// } + } \ No newline at end of file From ec8de40700b4463d82fc5e32b74ce2f0c284cd71 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Thu, 4 Jan 2024 03:34:59 +0900 Subject: [PATCH 67/82] =?UTF-8?q?Fix:=20=EC=97=B0=EA=B2=B0=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/StockRepository.java | 8 ++-- .../domain/product/dto/KafkaEventDto.java | 1 - .../domain/purchase/PurchaseController.java | 3 +- .../domain/purchase/PurchaseFacade.java | 31 -------------- .../domain/purchase/PurchaseService.java | 42 ++++++------------- .../purchase/entity/PurchaseDetail.java | 4 ++ .../global/config/KafkaConfig.java | 4 +- 7 files changed, 24 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index 1c76c09..4cbf87a 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -2,7 +2,9 @@ import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -10,7 +12,7 @@ @Repository public interface StockRepository extends JpaRepository { - - @Query("SELECT s FROM Stock s WHERE s.product IN (:productList)") - List findAllByProductIn(List productList); + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Stock s WHERE s.product.id IN :requestedProductsIds") + List findAllByProductIdIn(List requestedProductsIds); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/KafkaEventDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/KafkaEventDto.java index e0f3757..3c22d65 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/KafkaEventDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/KafkaEventDto.java @@ -19,7 +19,6 @@ public static KafkaEventDto from(Product product) { return KafkaEventDto.builder() .name(product.getName()) .price(product.getPrice()) - .stock(product.getStock()) .discountRate(product.getDiscountRate()) .build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java index d2a516f..a0db8d8 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseController.java @@ -17,12 +17,11 @@ public class PurchaseController { private final PurchaseService purchaseService; - private final PurchaseFacade purchaseFacade; @PostMapping public ResponseEntity purchaseProducts(@RequestBody @Validated PurchaseRequestDto requestDto, @LoginAccount Member member) { - purchaseFacade.purchaseProducts(requestDto.purchaseList(), member); + purchaseService.purchaseProducts(requestDto.purchaseList(), member); return ResponseEntity.status(HttpStatus.CREATED).build(); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java deleted file mode 100644 index b38e755..0000000 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseFacade.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.purebasketbe.domain.purchase; - -import com.example.purebasketbe.domain.member.entity.Member; -import com.example.purebasketbe.domain.purchase.PurchaseService; -import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; -import com.example.purebasketbe.global.kafka.KafkaService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class PurchaseFacade { - - private final PurchaseService purchaseService; - private final KafkaService kafkaService; - - public void purchaseProducts(List purchaseRequestDto, Member member) { - - List requestedProductsIds = purchaseRequestDto.stream() - .map(PurchaseDetail::productId).toList(); - purchaseService.processPurchase(purchaseRequestDto, requestedProductsIds); - - log.info("회원 {}: 상품 구매 요청 적재", member.getId()); - kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); - } - -} diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 56bec57..7438270 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -10,6 +10,7 @@ import com.example.purebasketbe.domain.purchase.entity.Purchase; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; +import com.example.purebasketbe.global.kafka.KafkaService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -28,46 +29,33 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; - private final ProductRepository productRepository; private final StockRepository stockRepository; + private final KafkaService kafkaService; private final int PRODUCTS_PER_PAGE = 10; @Transactional - public void processPurchase(List purchaseRequestDto, List requestedProductsIds) { + public void purchaseProducts(List purchaseRequestDto, Member member) { int size = purchaseRequestDto.size(); - List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); + // Lock 적용 + List requestedProductsIds = purchaseRequestDto.stream() + .map(PurchaseDetail::productId).toList(); + List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); + + List validProductList = stockList.stream().map(Stock::getProduct).toList(); validateProducts(size, validProductList); - List stockList = stockRepository.findAllByProductIn(validProductList); List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); - for (int i = 0; i < size; i++) { Stock stock = stockList.get(i); int amount = amountList.get(i); checkProductStock(stock, amount); stock.decrementStock(amount); } -// productBatchUpdate(updatedProductList, amountList); - } -// public void productBatchUpdate(List productList, List amountList) { -// String sql = "UPDATE product SET stock = stock - ?, sales_count = sales_count + ? WHERE id = ?"; -// -// jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { -// @Override -// public void setValues(PreparedStatement ps, int i) throws SQLException { -// ps.setLong(1, amountList.get(i)); -// ps.setLong(2, amountList.get(i)); -// ps.setLong(3, productList.get(i).getId()); -// } -// @Override -// public int getBatchSize() { -// return productList.size(); -// } -// }); -// -// } + kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); + log.info("회원 {}: 상품 구매 요청 적재", member.getId()); + } @Transactional(readOnly = true) public Page getPurchases(Member member, int page, String sortBy, String order) { @@ -91,10 +79,4 @@ private static void checkProductStock(Stock stock, int amount) { throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); } } - - private Product getProductById(Long id) { - return productRepository.findById(id).orElseThrow( - () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) - ); - } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java index 3818d38..8451a44 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/PurchaseDetail.java @@ -20,6 +20,10 @@ public class PurchaseDetail { @JoinColumn(name = "product_id") private Product product; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "purchase_id") + private Purchase purchase; + @Min(value = 1) @Column(nullable = false) private int amount; diff --git a/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java b/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java index eeb1be6..10d72af 100644 --- a/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/KafkaConfig.java @@ -28,7 +28,7 @@ public class KafkaConfig { @Bean public NewTopic saleEventTopic() { return TopicBuilder.name("event") - .partitions(2) + .partitions(3) .replicas(2) .build(); } @@ -36,7 +36,7 @@ public NewTopic saleEventTopic() { @Bean public NewTopic purchaseTopic() { return TopicBuilder.name("purchase") - .partitions(2) + .partitions(3) // .replicas(1) .build(); } From 0f7b79586397c36c57b591b584f2bb6232e6db4f Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Thu, 4 Jan 2024 14:02:13 +0900 Subject: [PATCH 68/82] =?UTF-8?q?Update:=20redis=20sentinel=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EC=97=90=EC=84=9C=20=EB=8B=A4=EC=8B=9C=20master-repli?= =?UTF-8?q?ca=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20revert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductService.java | 2 + .../domain/purchase/entity/Purchase.java | 7 -- .../global/config/RedisCacheConfig.java | 71 +++++++++++++++++ .../global/config/RedisConfig.java | 78 ++----------------- 4 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index b2895a1..ab5d33c 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -13,6 +13,7 @@ import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; @@ -28,6 +29,7 @@ @Service @RequiredArgsConstructor +@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java index ec3e1d2..2526c20 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/entity/Purchase.java @@ -36,11 +36,4 @@ private Purchase(Member member, int totalPrice) { this.member = member; this.totalPrice = totalPrice; } -// -// public static Purchase of(Member member, int totalPrice) { -// return Purchase.builder() -// .member(member) -// .totalPrice(totalPrice) -// .build(); -// } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java new file mode 100644 index 0000000..c54ceeb --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java @@ -0,0 +1,71 @@ +package com.example.purebasketbe.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@EnableCaching +@Configuration +@RequiredArgsConstructor +public class RedisCacheConfig { + private final RedisProperties redisProperties; +// @Value("${spring.cache.redis.host}") +// private String cacheHost; +// +// @Value("${spring.cache.redis.port}") +// private int cachePort; + + @Bean(name = "redisCacheConnectionFactory") + public RedisConnectionFactory redisCacheConnectionFactory() { +// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); +// redisStandaloneConfiguration.setHostName(cacheHost); +// redisStandaloneConfiguration.setPort(cachePort); +// return new LettuceConnectionFactory(redisStandaloneConfiguration); + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + + @Bean(name = "redisCacheTemplate") + public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(connectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + + return redisTemplate; + } + + @Bean + public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { + RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()).build(); + return redisCacheManager; + } + + private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { + return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(300)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java index bc1d814..56819ea 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -1,14 +1,15 @@ package com.example.purebasketbe.global.config; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -22,36 +23,12 @@ @Configuration @EnableRedisRepositories public class RedisConfig { - @Value("${spring.data.redis.sentinel.master}") - private String sentinel1; - @Value("${spring.data.redis.sentinel.replica1}") - private String sentinel2; - @Value("${spring.data.redis.sentinel.replica2}") - private String sentinel3; - @Value("${spring.data.redis.sentinel.port}") - private int port; - - // local에서만 필요한 포트 - @Value("${spring.data.redis.sentinel.port2}") - private int port2; - @Value("${spring.data.redis.sentinel.port3}") - private int port3; - - @Value("${spring.data.redis.password}") - private String password; - + private final RedisProperties redisProperties; + // RedisProperties로 yaml에 저장한 host, post를 연결 @Bean public RedisConnectionFactory redisConnectionFactory() { - RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() - .master("mymaster") - .sentinel(sentinel1, port) - .sentinel(sentinel2, port2) - .sentinel(sentinel3, port3); - - sentinelConfig.setPassword(password); - return new LettuceConnectionFactory(sentinelConfig ); - + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); } // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 @@ -65,48 +42,5 @@ public RedisTemplate redisTemplate() { return redisTemplate; } - @Bean - public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { - RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration()).build(); - return redisCacheManager; - } - - private RedisCacheConfiguration redisCacheConfiguration() { - return RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofHours(24)) - .disableCachingNullValues() - .serializeKeysWith( - RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) - ) - .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) - ); - } - -// @Bean -// @Primary -// public LettuceConnectionFactory redisConnectionFactory() { -// return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port)); -// } - -// @Bean(name = "redisCacheConnectionFactory") -// public RedisConnectionFactory redisCacheConnectionFactory() { -// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); -// redisStandaloneConfiguration.setHostName(cacheHost); -// redisStandaloneConfiguration.setPort(cachePort); -// return new LettuceConnectionFactory(redisStandaloneConfiguration); -// } - -// @Bean(name = "redisCacheTemplate") -// public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { -// RedisTemplate redisTemplate = new RedisTemplate<>(); -// redisTemplate.setConnectionFactory(connectionFactory); -// redisTemplate.setKeySerializer(new StringRedisSerializer()); -// redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); -// -// return redisTemplate; -// } } \ No newline at end of file From 037b466e11f48bd30201ddae826886add854a62f Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 5 Jan 2024 01:24:08 +0900 Subject: [PATCH 69/82] =?UTF-8?q?Fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=B6=94=EA=B0=80=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EB=B3=84=EB=A1=9C=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EC=B2=B4=ED=81=AC=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/domain/cart/CartRepository.java | 4 ++-- .../com/example/purebasketbe/domain/cart/CartService.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java index 740a93f..4cb3dd0 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartRepository.java @@ -14,8 +14,8 @@ @Repository public interface CartRepository extends JpaRepository { - @Query("select exists (select c.id from Cart c where c.product = :product)") - boolean existsProduct(Product product); + @Query("select exists (select c.id from Cart c where c.product = :product and c.member = :member)") + boolean existsProductByMember(Product product, Member member); @Modifying(clearAutomatically = true) @Query("DELETE FROM Cart c WHERE c.member=:member AND c.product IN (:products)") diff --git a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java index e89d7cf..b9dceb2 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/CartService.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/CartService.java @@ -32,7 +32,7 @@ public class CartService { @Transactional public void addToCart(Long productId, CartRequestDto requestDto, Member member) { Product product = findAndValidateProduct(productId); - checkIfExist(product); + checkIfExist(product, member); Cart newCart = Cart.of(product, member, requestDto); cartRepository.save(newCart); } @@ -73,8 +73,8 @@ private Product findAndValidateProduct(Long productId) { ); } - private void checkIfExist(Product product) { - if (cartRepository.existsProduct(product)) { + private void checkIfExist(Product product, Member member) { + if (cartRepository.existsProductByMember(product, member)) { throw new CustomException(ErrorCode.PRODUCT_ALREADY_ADDED); } } From 3bd7290fbde102cee601f415be482b77b57e2f8a Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 5 Jan 2024 18:13:21 +0900 Subject: [PATCH 70/82] =?UTF-8?q?Update:=20PurchaseResponseDto=EC=97=90=20?= =?UTF-8?q?id=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/purchase/dto/PurchaseResponseDto.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java index facc5ef..1f5421a 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/dto/PurchaseResponseDto.java @@ -17,6 +17,7 @@ public record PurchaseResponseDto( public static PurchaseResponseDto from(Purchase purchase) { return PurchaseResponseDto.builder() + .id(purchase.getId()) .totalPrice(purchase.getTotalPrice()) .purchasedAt(purchase.getPurchasedAt()) .build(); From 5333f3c3abf8b56ddef392049a5491368da11371 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 5 Jan 2024 18:13:41 +0900 Subject: [PATCH 71/82] =?UTF-8?q?Update:=20=EC=9E=AC=EA=B3=A0=20=EC=B0=A8?= =?UTF-8?q?=EA=B0=90=20=EB=A1=9C=EC=A7=81=20consumer=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/StockRepository.java | 5 ---- .../domain/product/entity/Stock.java | 3 -- .../domain/purchase/PurchaseService.java | 30 ++++++++++--------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index fcbe790..a02c331 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -1,6 +1,5 @@ package com.example.purebasketbe.domain.product; -import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,8 +12,4 @@ @Repository public interface StockRepository extends JpaRepository { - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT s FROM Stock s WHERE s.product.id IN :requestedProductsIds") - List findAllByProductIdIn(List requestedProductsIds); - } diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java index d79bc2e..852a622 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Stock.java @@ -40,8 +40,5 @@ public void update(int stock) { this.stock = stock; } - public void decrementStock(int amount) { - this.stock -= amount; - } } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index f8b1e56..7581b63 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -1,6 +1,7 @@ package com.example.purebasketbe.domain.purchase; import com.example.purebasketbe.domain.member.entity.Member; +import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.StockRepository; import com.example.purebasketbe.domain.product.entity.Product; import com.example.purebasketbe.domain.product.entity.Stock; @@ -29,6 +30,7 @@ public class PurchaseService { private final PurchaseRepository purchaseRepository; private final StockRepository stockRepository; + private final ProductRepository productRepository; private final KafkaService kafkaService; private final int PRODUCTS_PER_PAGE = 10; @@ -39,18 +41,18 @@ public void purchaseProducts(List purchaseRequestDto, Member mem // Lock 적용 List requestedProductsIds = purchaseRequestDto.stream() .map(PurchaseDetail::productId).toList(); - List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); +// List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); - List validProductList = stockList.stream().map(Stock::getProduct).toList(); + List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); validateProducts(size, validProductList); - List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); - for (int i = 0; i < size; i++) { - Stock stock = stockList.get(i); - int amount = amountList.get(i); - checkProductStock(stock, amount); - stock.decrementStock(amount); - } +// List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); +// for (int i = 0; i < size; i++) { +// Stock stock = stockList.get(i); +// int amount = amountList.get(i); +// checkProductStock(stock, amount); +// stock.decrementStock(amount); +// } kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); log.info("회원 {}: 상품 구매 요청 적재", member.getId()); @@ -73,9 +75,9 @@ private static void validateProducts(int size, List validProductList) { } } - private static void checkProductStock(Stock stock, int amount) { - if (stock.getStock() < amount) { - throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); - } - } +// private static void checkProductStock(Stock stock, int amount) { +// if (stock.getStock() < amount) { +// throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); +// } +// } } \ No newline at end of file From cd64394fcd8cc1473f3b8c5444897218caa26943 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Fri, 5 Jan 2024 18:14:31 +0900 Subject: [PATCH 72/82] =?UTF-8?q?Update:=20CORS=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/global/config/WebSecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java index 8fbd3c2..927085f 100644 --- a/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/WebSecurityConfig.java @@ -96,7 +96,7 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); - config.setAllowedOrigins(List.of("http://localhost:3000")); + config.setAllowedOrigins(List.of("http://localhost:3000", "https://pure-basket.vercel.app/")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); config.setAllowedHeaders(List.of("*")); config.setExposedHeaders(List.of("*")); From 1561529bf994ffdee9ad1a0a0501ac1eec2025cb Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Sat, 6 Jan 2024 01:04:52 +0900 Subject: [PATCH 73/82] =?UTF-8?q?Chore:=20elk=20dependency=20=EB=B0=8F=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?[#38]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++ src/main/resources/elastic-settings.json | 9 +++++ src/main/resources/logback-spring.xml | 42 ++++++++++++++++++++++++ src/main/resources/product-mappings.json | 23 +++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/resources/elastic-settings.json create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/product-mappings.json diff --git a/build.gradle b/build.gradle index 91b6564..bfd7763 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,11 @@ dependencies { implementation 'io.micrometer:micrometer-core' implementation 'io.micrometer:micrometer-registry-prometheus' + // elk es 8.11.1 + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + implementation 'com.github.danielwegener:logback-kafka-appender:0.2.0-RC2' + // email service implementation 'org.springframework.boot:spring-boot-starter-mail' diff --git a/src/main/resources/elastic-settings.json b/src/main/resources/elastic-settings.json new file mode 100644 index 0000000..b400a4f --- /dev/null +++ b/src/main/resources/elastic-settings.json @@ -0,0 +1,9 @@ +{ + "analysis": { + "analyzer": { + "korean": { + "type": "nori" + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..c3d900c --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,42 @@ + + + + + + + + + + "%d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{5} - %msg %n" + + + + + ${dest-logstash} + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %magenta([%thread]) %highlight([%-3level]) %logger{36} - %msg %n + + + logs + + + + bootstrap.servers=${dest-kafka} + acks=0 + linger.ms=100 + max.block.ms=100 + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/product-mappings.json b/src/main/resources/product-mappings.json new file mode 100644 index 0000000..85bba97 --- /dev/null +++ b/src/main/resources/product-mappings.json @@ -0,0 +1,23 @@ +{ + "properties": { + "id": { + "type": "long" + }, + "name": { + "type": "text", + "analyzer": "korean" + }, + "info": { + "type": "keyword", + "analyzer": "korean" + }, + "category": { + "type": "keyword", + "analyzer": "korean" + }, + "event": { + "type": "match_only_text", + "analyzer": "korean" + } + } +} \ No newline at end of file From 68745a565024ac6ee2f826ddf33e1a904532e441 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Sat, 6 Jan 2024 01:10:46 +0900 Subject: [PATCH 74/82] =?UTF-8?q?Feat:=20elk=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=201=EC=B0=A8=20=EA=B5=AC=ED=98=84=20[#38]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/product/ProductController.java | 10 ++++ .../domain/product/ProductService.java | 51 ++++++++++++++++--- .../product/dto/ProductResponseDto.java | 11 ++++ .../product/entity/ProductDocument.java | 47 +++++++++++++++++ .../global/config/ElasticSearchConfig.java | 34 +++++++++++++ 5 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java create mode 100644 src/main/java/com/example/purebasketbe/global/config/ElasticSearchConfig.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java index 9149cc0..0113da5 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java @@ -32,6 +32,16 @@ public ResponseEntity searchProducts( return ResponseEntity.status(HttpStatus.OK).body(responseBody); } + @GetMapping("/search/es") + public ResponseEntity searchProductsByES( + @RequestParam String query, + @RequestParam(defaultValue = "", required = false) String category, + @RequestParam(defaultValue = "1", required = false) int eventPage, + @RequestParam(defaultValue = "1", required = false) int page) { + ProductListResponseDto responseBody = productService.searchProductsByES(query, category, eventPage - 1, page - 1); + return ResponseEntity.status(HttpStatus.OK).body(responseBody); + } + @GetMapping("/{productId}") public ResponseEntity getProduct(@PathVariable Long productId) { ProductResponseDto responseBody = productService.getProduct(productId); diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index ab5d33c..495016e 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -3,23 +3,26 @@ import com.example.purebasketbe.domain.product.dto.ProductListResponseDto; import com.example.purebasketbe.domain.product.dto.ProductRequestDto; import com.example.purebasketbe.domain.product.dto.ProductResponseDto; -import com.example.purebasketbe.domain.product.entity.Event; -import com.example.purebasketbe.domain.product.entity.Image; -import com.example.purebasketbe.domain.product.entity.Product; -import com.example.purebasketbe.domain.product.entity.Stock; +import com.example.purebasketbe.domain.product.entity.*; import com.example.purebasketbe.global.RestPageImpl; import com.example.purebasketbe.global.exception.CustomException; import com.example.purebasketbe.global.exception.ErrorCode; import com.example.purebasketbe.global.kafka.KafkaService; import com.example.purebasketbe.global.s3.S3Handler; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHitSupport; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.SearchPage; +import org.springframework.data.elasticsearch.core.query.Criteria; +import org.springframework.data.elasticsearch.core.query.CriteriaQuery; +import org.springframework.data.elasticsearch.core.query.Query; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -29,7 +32,6 @@ @Service @RequiredArgsConstructor -@Qualifier("redisCacheTemplate") public class ProductService { private final ProductRepository productRepository; @@ -37,6 +39,7 @@ public class ProductService { private final StockRepository stockRepository; private final S3Handler s3Handler; private final KafkaService kafkaHandler; + private final ElasticsearchOperations elasticsearchOperations; @Value("${products.event.page.size}") private int eventPageSize; @@ -83,6 +86,32 @@ public ProductListResponseDto searchProducts(String query, String category, int return ProductListResponseDto.of(eventProductsResponse, productsResponse); } + @Transactional(readOnly = true) + public ProductListResponseDto searchProductsByES(String query, String category, int eventPage, int page) { + Pageable eventPageable = getPageable(eventPage, eventPageSize); + Pageable pageable = getPageable(page, pageSize); + + Criteria eventCriteria; + if (category.isEmpty()) { + eventCriteria = new Criteria("name").contains(query).and("event").is("DISCOUNT").and("category").is(category); + } else { + eventCriteria = new Criteria("name").contains(query).and("event").is("DISCOUNT"); + } + SearchPage eventProducts = getPagedSearchResults(eventCriteria, eventPageable); + + Criteria criteria; + if (category.isEmpty()) { + criteria = new Criteria("name").contains(query).and("event").is("NORMAL").and("category").is(category); + } else { + criteria = new Criteria("name").contains(query).and("event").is("NORMAL"); + } + SearchPage products = getPagedSearchResults(criteria, pageable); + + Page eventProductsResponse = eventProducts.map(ProductResponseDto::from); + Page productsResponse = products.map(ProductResponseDto::from); + + return ProductListResponseDto.of(eventProductsResponse, productsResponse); + } @Transactional(readOnly = true) public ProductResponseDto getProduct(Long productId) { @@ -102,6 +131,8 @@ public void registerProduct(ProductRequestDto requestDto, List fi stockRepository.save(stock); saveAndUploadImage(newProduct, files); + elasticsearchOperations.save(ProductDocument.from(newProduct)); + if (newProduct.getEvent().equals(Event.DISCOUNT)) { kafkaHandler.sendEventToKafka(ProductResponseDto.from(newProduct)); } @@ -142,6 +173,14 @@ private Page getResponseDtoFromProducts(Page produc }); } + private SearchPage getPagedSearchResults(Criteria criteria, Pageable pageable) { + Query searchQuery = new CriteriaQuery(criteria); + SearchHits searchHits = elasticsearchOperations.search(searchQuery, ProductDocument.class); + return SearchHitSupport.searchPageFor(searchHits, pageable); +// SearchPage searchPage = SearchHitSupport.searchPageFor(searchHits, pageable); +// return (Page) SearchHitSupport.unwrapSearchHits(searchPage); + } + private Pageable getPageable(int page, int pageSize) { Sort sort = Sort.by(Sort.Direction.DESC, "modifiedAt"); return PageRequest.of(page, pageSize, sort); diff --git a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java index 96e072f..71a16df 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/product/dto/ProductResponseDto.java @@ -2,8 +2,10 @@ import com.example.purebasketbe.domain.product.entity.Event; import com.example.purebasketbe.domain.product.entity.Product; +import com.example.purebasketbe.domain.product.entity.ProductDocument; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; +import org.springframework.data.elasticsearch.core.SearchHit; import java.util.List; @@ -47,4 +49,13 @@ public static ProductResponseDto from(Product product) { .discountRate(product.getDiscountRate()) .build(); } + public static ProductResponseDto from(SearchHit searchHit) { + return ProductResponseDto.builder() + .id(searchHit.getContent().getId()) + .name(searchHit.getContent().getName()) + .info(searchHit.getContent().getInfo()) + .category(searchHit.getContent().getCategory()) + .event(searchHit.getContent().getEvent()) + .build(); + } } diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java b/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java new file mode 100644 index 0000000..32faa4b --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java @@ -0,0 +1,47 @@ +package com.example.purebasketbe.domain.product.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Mapping; +import org.springframework.data.elasticsearch.annotations.Setting; + +@Getter +@Document(indexName = "product_doc") +@Setting(settingPath = "/elastic-settings.json") +@Mapping(mappingPath = "/product-mappings.json") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductDocument { + @Id + private Long id; + + private String name; + + private String info; + + private String category; + + private Event event; + + @Builder + private ProductDocument(Long id, String name, String info, String category, Event event) { + this.id = id; + this.name = name; + this.info = info; + this.category = category; + this.event = event; + } + + public static ProductDocument from(Product product) { + return ProductDocument.builder() + .id(product.getId()) + .name(product.getName()) + .info(product.getInfo()) + .category(product.getCategory()) + .event(product.getEvent()) + .build(); + } +} diff --git a/src/main/java/com/example/purebasketbe/global/config/ElasticSearchConfig.java b/src/main/java/com/example/purebasketbe/global/config/ElasticSearchConfig.java new file mode 100644 index 0000000..2f5a102 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/global/config/ElasticSearchConfig.java @@ -0,0 +1,34 @@ +package com.example.purebasketbe.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.client.ClientConfiguration; +import org.springframework.data.elasticsearch.client.elc.ElasticsearchConfiguration; +import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +import org.springframework.data.elasticsearch.support.HttpHeaders; + + +@Configuration +@EnableElasticsearchRepositories +public class ElasticSearchConfig extends ElasticsearchConfiguration { + + @Value("${elasticsearch.uri}") + private String esUrl; + @Value("${elasticsearch.apiKey}") + private String apiKey; + @Value("${spring.elasticsearch.username") + private String username; + @Value("${spring.elasticsearch.password") + private String password; + + @Override + public ClientConfiguration clientConfiguration() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "ApiKey " + apiKey); + return ClientConfiguration.builder() + .connectedTo(esUrl) + .withDefaultHeaders(headers) +// .withBasicAuth(username, password) + .build(); + } +} From adc443ab795d10f6585dc93f37c0ed9f7a620dd0 Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Sat, 6 Jan 2024 14:07:53 +0900 Subject: [PATCH 75/82] =?UTF-8?q?Update:=20CartResponseDto=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/cart/dto/CartResponseDto.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java index c79ab6f..02eff25 100644 --- a/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java +++ b/src/main/java/com/example/purebasketbe/domain/cart/dto/CartResponseDto.java @@ -1,6 +1,8 @@ package com.example.purebasketbe.domain.cart.dto; import com.example.purebasketbe.domain.cart.entity.Cart; +import com.example.purebasketbe.domain.product.entity.Event; +import com.example.purebasketbe.domain.product.entity.Product; import lombok.Builder; public record CartResponseDto( @@ -9,7 +11,12 @@ public record CartResponseDto( Integer price, String category, String imageUrl, - int amount + int amount, + + Event event, + + int discountRate + ) { @Builder public CartResponseDto { @@ -18,12 +25,15 @@ public record CartResponseDto( public static CartResponseDto from( Cart cart) { + Product product = cart.getProduct(); return CartResponseDto.builder() - .id(cart.getProduct().getId()) - .name(cart.getProduct().getName()) - .price(cart.getProduct().getPrice()) - .category(cart.getProduct().getCategory()) - .imageUrl(cart.getProduct().getImages().get(0).getImgUrl()) + .id(product.getId()) + .name(product.getName()) + .price(product.getPrice()) + .category(product.getCategory()) + .imageUrl(product.getImages().get(0).getImgUrl()) + .event(product.getEvent()) + .discountRate(product.getDiscountRate()) .amount(cart.getAmount()) .build(); } From ccccf28b3c0a8d4b22921ac89aa6c8757f73c709 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 8 Jan 2024 12:13:30 +0900 Subject: [PATCH 76/82] =?UTF-8?q?Fix:=20=EA=B2=80=EC=83=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=88=98=EC=A0=95=20[#38]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit if문 오류 수정 --- .../purebasketbe/domain/product/ProductController.java | 2 +- .../purebasketbe/domain/product/ProductService.java | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java index 0113da5..3152243 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java @@ -34,7 +34,7 @@ public ResponseEntity searchProducts( @GetMapping("/search/es") public ResponseEntity searchProductsByES( - @RequestParam String query, + @RequestParam(defaultValue = "", required = false) String query, @RequestParam(defaultValue = "", required = false) String category, @RequestParam(defaultValue = "1", required = false) int eventPage, @RequestParam(defaultValue = "1", required = false) int page) { diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index 495016e..b0c011b 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -93,17 +93,17 @@ public ProductListResponseDto searchProductsByES(String query, String category, Criteria eventCriteria; if (category.isEmpty()) { - eventCriteria = new Criteria("name").contains(query).and("event").is("DISCOUNT").and("category").is(category); - } else { eventCriteria = new Criteria("name").contains(query).and("event").is("DISCOUNT"); + } else { + eventCriteria = new Criteria("name").contains(query).and("event").is("DISCOUNT").and("category").is(category); } SearchPage eventProducts = getPagedSearchResults(eventCriteria, eventPageable); Criteria criteria; if (category.isEmpty()) { - criteria = new Criteria("name").contains(query).and("event").is("NORMAL").and("category").is(category); - } else { criteria = new Criteria("name").contains(query).and("event").is("NORMAL"); + } else { + criteria = new Criteria("name").contains(query).and("event").is("NORMAL").and("category").is(category); } SearchPage products = getPagedSearchResults(criteria, pageable); @@ -177,8 +177,6 @@ private SearchPage getPagedSearchResults(Criteria criteria, Pag Query searchQuery = new CriteriaQuery(criteria); SearchHits searchHits = elasticsearchOperations.search(searchQuery, ProductDocument.class); return SearchHitSupport.searchPageFor(searchHits, pageable); -// SearchPage searchPage = SearchHitSupport.searchPageFor(searchHits, pageable); -// return (Page) SearchHitSupport.unwrapSearchHits(searchPage); } private Pageable getPageable(int page, int pageSize) { From bd25c2a53b7d1a153a6bdeeb87971336dfd750fd Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 8 Jan 2024 13:36:39 +0900 Subject: [PATCH 77/82] =?UTF-8?q?Update:=20=EA=B8=B0=EB=B3=B8=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20nullable=EB=A1=9C=20=EC=88=98=EC=A0=95=20[#38]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/purebasketbe/domain/product/ProductController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java index 3152243..9162924 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductController.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductController.java @@ -24,7 +24,7 @@ public ResponseEntity getProducts( @GetMapping("/search") public ResponseEntity searchProducts( - @RequestParam String query, + @RequestParam(defaultValue = "", required = false) String query, @RequestParam(defaultValue = "", required = false) String category, @RequestParam(defaultValue = "1", required = false) int eventPage, @RequestParam(defaultValue = "1", required = false) int page) { From 0c719b9531c7c1eea33ad985af15e298c6ef50f3 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 8 Jan 2024 23:53:37 +0900 Subject: [PATCH 78/82] =?UTF-8?q?Refactor:=20Product-Stock=20=EB=8B=A8?= =?UTF-8?q?=EB=B0=A9=ED=96=A5=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../purebasketbe/domain/product/StockRepository.java | 7 +------ .../purebasketbe/domain/product/entity/Product.java | 10 +--------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index a02c331..0af4817 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -1,15 +1,10 @@ package com.example.purebasketbe.domain.product; import com.example.purebasketbe.domain.product.entity.Stock; -import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface StockRepository extends JpaRepository { - + Stock findByProductId(Long productId); } diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java index 1452fc5..1c8a122 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/Product.java @@ -49,16 +49,13 @@ public class Product { @Column(nullable = false) private boolean deleted; - @OneToOne(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) - private Stock stock; - @BatchSize(size = 21) @OneToMany(mappedBy = "product", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true) private List images = new ArrayList<>(); @Builder private Product(String name, Integer price, String info, - String category, Event event, Integer discountRate, Stock stock) { + String category, Event event, Integer discountRate) { this.name = name; this.price = price; this.info = info; @@ -68,7 +65,6 @@ private Product(String name, Integer price, String info, this.createdAt = LocalDateTime.now(); this.modifiedAt = LocalDateTime.now(); this.deleted = false; - this.stock = stock; } public static Product from(ProductRequestDto requestDto) { @@ -97,8 +93,4 @@ public void softDelete() { this.modifiedAt = LocalDateTime.now(); this.deleted = true; } - - public void attachStock(Stock stock) { - this.stock = stock; - } } \ No newline at end of file From 1c62c432cf0190d0d9c86c53e625e708efb31f48 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Mon, 8 Jan 2024 23:55:34 +0900 Subject: [PATCH 79/82] =?UTF-8?q?Fix:=20ES=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20Paging=20=EC=88=98=EC=A0=95=20[#38]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/ProductSearchRepository.java | 10 +++++ .../domain/product/ProductService.java | 14 +++---- .../product/entity/ProductDocument.java | 38 +++++++++++++++---- src/main/resources/product-mappings.json | 21 ++++++++-- 4 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/example/purebasketbe/domain/product/ProductSearchRepository.java diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductSearchRepository.java b/src/main/java/com/example/purebasketbe/domain/product/ProductSearchRepository.java new file mode 100644 index 0000000..c3c7b50 --- /dev/null +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductSearchRepository.java @@ -0,0 +1,10 @@ +package com.example.purebasketbe.domain.product; + +import com.example.purebasketbe.domain.product.entity.ProductDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductSearchRepository extends ElasticsearchRepository { + +} diff --git a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java index b0c011b..eb8ac63 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/ProductService.java +++ b/src/main/java/com/example/purebasketbe/domain/product/ProductService.java @@ -12,10 +12,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.SearchHitSupport; import org.springframework.data.elasticsearch.core.SearchHits; @@ -125,7 +122,6 @@ public void registerProduct(ProductRequestDto requestDto, List fi checkExistProductByName(requestDto.name()); Product newProduct = Product.from(requestDto); Stock stock = Stock.of(requestDto, newProduct); - newProduct.attachStock(stock); productRepository.save(newProduct); stockRepository.save(stock); @@ -141,7 +137,7 @@ public void registerProduct(ProductRequestDto requestDto, List fi @Transactional public void updateProduct(Long productId, ProductRequestDto requestDto, List files) { Product product = findProduct(productId); - Stock stock = product.getStock(); + Stock stock = findStock(productId); product.update(requestDto); if (requestDto.stock() != null) { @@ -174,7 +170,7 @@ private Page getResponseDtoFromProducts(Page produc } private SearchPage getPagedSearchResults(Criteria criteria, Pageable pageable) { - Query searchQuery = new CriteriaQuery(criteria); + Query searchQuery = new CriteriaQuery(criteria, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize())); SearchHits searchHits = elasticsearchOperations.search(searchQuery, ProductDocument.class); return SearchHitSupport.searchPageFor(searchHits, pageable); } @@ -212,4 +208,8 @@ private Product findProduct(Long id) { () -> new CustomException(ErrorCode.PRODUCT_NOT_FOUND) ); } + + private Stock findStock(Long productId) { + return stockRepository.findByProductId(productId); + } } \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java b/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java index 32faa4b..e4b8600 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java +++ b/src/main/java/com/example/purebasketbe/domain/product/entity/ProductDocument.java @@ -5,9 +5,9 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Mapping; -import org.springframework.data.elasticsearch.annotations.Setting; +import org.springframework.data.elasticsearch.annotations.*; + +import java.time.LocalDateTime; @Getter @Document(indexName = "product_doc") @@ -16,32 +16,54 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ProductDocument { @Id +// @Field(type = FieldType.Long) private Long id; - + // @Field(type = FieldType.Text) private String name; - + // @Field(type = FieldType.Integer) + private int price; + // @Field(type = FieldType.Text) private String info; - + // @Field(type = FieldType.Text) private String category; - + // @Field(type = FieldType.Text) private Event event; + // @Field(type = FieldType.Integer) + private int discountRate; + // @Field(type = FieldType.Date) + private LocalDateTime createdAt; + // @Field(type = FieldType.Date) + private LocalDateTime modifiedAt; + // @Field(type = FieldType.Boolean) + private boolean deleted; @Builder - private ProductDocument(Long id, String name, String info, String category, Event event) { + private ProductDocument(Long id, String name, int price, String info, String category, Event event, + int discountRate, LocalDateTime createdAt, LocalDateTime modifiedAt, boolean deleted) { this.id = id; this.name = name; + this.price = price; this.info = info; this.category = category; this.event = event; + this.discountRate = discountRate; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + this.deleted = deleted; } public static ProductDocument from(Product product) { return ProductDocument.builder() .id(product.getId()) .name(product.getName()) + .price(product.getPrice()) .info(product.getInfo()) .category(product.getCategory()) .event(product.getEvent()) + .discountRate(product.getDiscountRate()) + .createdAt(product.getCreatedAt()) + .modifiedAt(product.getModifiedAt()) + .deleted(product.isDeleted()) .build(); } } diff --git a/src/main/resources/product-mappings.json b/src/main/resources/product-mappings.json index 85bba97..5ddf8c2 100644 --- a/src/main/resources/product-mappings.json +++ b/src/main/resources/product-mappings.json @@ -7,17 +7,32 @@ "type": "text", "analyzer": "korean" }, + "price": { + "type": "integer" + }, "info": { - "type": "keyword", + "type": "text", "analyzer": "korean" }, "category": { - "type": "keyword", + "type": "text", "analyzer": "korean" }, "event": { - "type": "match_only_text", + "type": "text", "analyzer": "korean" + }, + "discountRate": { + "type": "integer" + }, + "createdAt": { + "type": "date" + }, + "modifiedAt": { + "type": "date" + }, + "deleted": { + "type": "boolean" } } } \ No newline at end of file From 2ea631dfb2c7e92422c552fe9e2d821248d0f161 Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Wed, 10 Jan 2024 12:35:48 +0900 Subject: [PATCH 80/82] =?UTF-8?q?Refactor:=20DB=20Lock=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조회 비관적락을 삭제하고 Update 쿼리로 수정 --- .../domain/product/StockRepository.java | 8 +++--- .../domain/purchase/PurchaseService.java | 27 ++++++------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java index a02c331..f2a0ad6 100644 --- a/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java +++ b/src/main/java/com/example/purebasketbe/domain/product/StockRepository.java @@ -1,15 +1,15 @@ package com.example.purebasketbe.domain.product; import com.example.purebasketbe.domain.product.entity.Stock; -import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.util.List; - @Repository public interface StockRepository extends JpaRepository { + @Modifying + @Query("update Stock s set s.stock = s.stock - :amount where s.stock > :amount AND s.product.id = :productId") + void updateStockByAmountByProductId(int amount, long productId); } diff --git a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java index 7581b63..a39305c 100644 --- a/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java +++ b/src/main/java/com/example/purebasketbe/domain/purchase/PurchaseService.java @@ -4,7 +4,6 @@ import com.example.purebasketbe.domain.product.ProductRepository; import com.example.purebasketbe.domain.product.StockRepository; import com.example.purebasketbe.domain.product.entity.Product; -import com.example.purebasketbe.domain.product.entity.Stock; import com.example.purebasketbe.domain.purchase.dto.PurchaseRequestDto.PurchaseDetail; import com.example.purebasketbe.domain.purchase.dto.PurchaseResponseDto; import com.example.purebasketbe.domain.purchase.entity.Purchase; @@ -19,6 +18,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -35,24 +35,19 @@ public class PurchaseService { private final int PRODUCTS_PER_PAGE = 10; - @Transactional + @Transactional(isolation = Isolation.REPEATABLE_READ) public void purchaseProducts(List purchaseRequestDto, Member member) { int size = purchaseRequestDto.size(); // Lock 적용 List requestedProductsIds = purchaseRequestDto.stream() .map(PurchaseDetail::productId).toList(); -// List stockList = stockRepository.findAllByProductIdIn(requestedProductsIds); - List validProductList = productRepository.findByIdInAndDeleted(requestedProductsIds, false); - validateProducts(size, validProductList); - -// List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); -// for (int i = 0; i < size; i++) { -// Stock stock = stockList.get(i); -// int amount = amountList.get(i); -// checkProductStock(stock, amount); -// stock.decrementStock(amount); -// } + List amountList = purchaseRequestDto.stream().map(PurchaseDetail::amount).toList(); + for (int i = 0; i < size; i++) { + Long productId = requestedProductsIds.get(i); + int amount = amountList.get(i); + stockRepository.updateStockByAmountByProductId(amount, productId); + } kafkaService.sendPurchaseToKafka(purchaseRequestDto, member); log.info("회원 {}: 상품 구매 요청 적재", member.getId()); @@ -74,10 +69,4 @@ private static void validateProducts(int size, List validProductList) { throw new CustomException(ErrorCode.PRODUCT_NOT_FOUND); } } - -// private static void checkProductStock(Stock stock, int amount) { -// if (stock.getStock() < amount) { -// throw new CustomException(ErrorCode.NOT_ENOUGH_PRODUCT); -// } -// } } \ No newline at end of file From 530b766b132e05ab86e7f518dbc2b2a4fd15bcbe Mon Sep 17 00:00:00 2001 From: Sanghyu Lee Date: Wed, 10 Jan 2024 14:48:39 +0900 Subject: [PATCH 81/82] =?UTF-8?q?Update:=20=EB=A1=9C=EA=B7=B8=EC=88=98?= =?UTF-8?q?=EC=A7=91=20=EC=9D=BC=EC=8B=9C=20=EC=A4=91=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kafka/elk 서버 스케일업 필요 --- src/main/resources/logback-spring.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index c3d900c..d0155c4 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,7 +1,7 @@ - + @@ -34,8 +34,8 @@ - - + + From 1d0c2f55a969cec45edbcafdf6ef5b55764dbe5d Mon Sep 17 00:00:00 2001 From: Joonyoung Hong Date: Wed, 10 Jan 2024 15:08:21 +0900 Subject: [PATCH 82/82] =?UTF-8?q?Update:=20Redis=20sentinel=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/RedisCacheConfig.java | 71 ------------------- .../global/config/RedisConfig.java | 48 +++++++++++-- 2 files changed, 42 insertions(+), 77 deletions(-) delete mode 100644 src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java deleted file mode 100644 index c54ceeb..0000000 --- a/src/main/java/com/example/purebasketbe/global/config/RedisCacheConfig.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.purebasketbe.global.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -@EnableCaching -@Configuration -@RequiredArgsConstructor -public class RedisCacheConfig { - private final RedisProperties redisProperties; -// @Value("${spring.cache.redis.host}") -// private String cacheHost; -// -// @Value("${spring.cache.redis.port}") -// private int cachePort; - - @Bean(name = "redisCacheConnectionFactory") - public RedisConnectionFactory redisCacheConnectionFactory() { -// RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); -// redisStandaloneConfiguration.setHostName(cacheHost); -// redisStandaloneConfiguration.setPort(cachePort); -// return new LettuceConnectionFactory(redisStandaloneConfiguration); - return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); - } - - @Bean(name = "redisCacheTemplate") - public RedisTemplate redisTemplate(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(connectionFactory); - redisTemplate.setKeySerializer(new StringRedisSerializer()); - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - - return redisTemplate; - } - - @Bean - public CacheManager redisCacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) { - RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder - .fromConnectionFactory(redisConnectionFactory) - .cacheDefaults(redisCacheConfiguration()).build(); - return redisCacheManager; - } - - private org.springframework.data.redis.cache.RedisCacheConfiguration redisCacheConfiguration() { - return org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofSeconds(300)) - .disableCachingNullValues() - .serializeKeysWith( - RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) - ) - .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java index 56819ea..e482d58 100644 --- a/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java +++ b/src/main/java/com/example/purebasketbe/global/config/RedisConfig.java @@ -1,15 +1,14 @@ package com.example.purebasketbe.global.config; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @@ -23,12 +22,30 @@ @Configuration @EnableRedisRepositories public class RedisConfig { - private final RedisProperties redisProperties; + @Value("${spring.data.redis.sentinel.master}") + private String sentinel1; + @Value("${spring.data.redis.sentinel.replica1}") + private String sentinel2; + @Value("${spring.data.redis.sentinel.replica2}") + private String sentinel3; + @Value("${spring.data.redis.sentinel.port}") + private int port; + + @Value("${spring.data.redis.password}") + private String password; + - // RedisProperties로 yaml에 저장한 host, post를 연결 @Bean public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration() + .master("mymaster") + .sentinel(sentinel1, port) + .sentinel(sentinel2, port) + .sentinel(sentinel3, port); + + sentinelConfig.setPassword(password); + return new LettuceConnectionFactory(sentinelConfig ); + } // serializer 설정으로 redis-cli를 통해 직접 데이터를 조회할 수 있도록 설정 @@ -42,5 +59,24 @@ public RedisTemplate redisTemplate() { return redisTemplate; } + @Bean + public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()).build(); + return redisCacheManager; + } + + private RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofHours(24)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } } \ No newline at end of file