From 2ae66fafc7b2ffe7cd5e2bc947e9f95427e0063c Mon Sep 17 00:00:00 2001 From: dldmldlsy Date: Wed, 24 Jan 2024 11:30:32 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EA=B4=80=EC=8B=AC=20=EC=A7=80?= =?UTF-8?q?=EC=97=AD=20=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관심지역 등록한 회원 ID 리스트 조회 메서드 구현 - 상품 등록 시, 해당 지역코드를 가진 회원들에 대해 알림 생성 --- .../alert/repository/AlertRepository.java | 1 + .../product/service/ProductService.java | 155 +++++++++++------- .../wish/repository/WishRegionRepository.java | 2 + .../user/wish/service/WishRegionService.java | 31 +++- 4 files changed, 121 insertions(+), 68 deletions(-) diff --git a/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java b/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java index 63ee1a18..5a828901 100644 --- a/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java +++ b/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java @@ -3,6 +3,7 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import site.goldenticket.domain.alert.entity.Alert; +import site.goldenticket.domain.product.constants.AreaCode; public interface AlertRepository extends JpaRepository { diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index c04255a3..385ea486 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -17,6 +17,7 @@ import site.goldenticket.common.constants.PaginationConstants; import site.goldenticket.common.exception.CustomException; import site.goldenticket.common.redis.service.RedisService; +import site.goldenticket.domain.alert.service.AlertService; import site.goldenticket.domain.product.constants.AreaCode; import site.goldenticket.domain.product.constants.PriceRange; import site.goldenticket.domain.product.constants.ProductStatus; @@ -25,6 +26,7 @@ import site.goldenticket.domain.product.repository.CustomSlice; import site.goldenticket.domain.product.repository.ProductRepository; import site.goldenticket.domain.security.PrincipalDetails; +import site.goldenticket.domain.user.wish.service.WishRegionService; import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; @@ -48,52 +50,59 @@ public class ProductService { private final RedisService redisService; private final ProductRepository productRepository; + private final AlertService alertService; + private final WishRegionService wishRegionService; // 1. 키워드 검색 및 지역 검색 메서드 @Transactional(readOnly = true) public Slice getProductsBySearch( - AreaCode areaCode, String keyword, LocalDate checkInDate, LocalDate checkOutDate, PriceRange priceRange, - LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, PrincipalDetails principalDetails + AreaCode areaCode, String keyword, LocalDate checkInDate, LocalDate checkOutDate, + PriceRange priceRange, + LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, + PrincipalDetails principalDetails ) { Long userId = (principalDetails != null) ? principalDetails.getUserId() : null; boolean isAuthenticated = (userId != null); CustomSlice productSlice = productRepository.getProductsBySearch( - areaCode, keyword, checkInDate, checkOutDate, priceRange, cursorCheckInDate, cursorId, pageable, userId + areaCode, keyword, checkInDate, checkOutDate, priceRange, cursorCheckInDate, cursorId, + pageable, userId ); SearchProductResponse searchProductResponse = SearchProductResponse.fromEntity( - areaCode, keyword, checkInDate, checkOutDate, priceRange, productSlice.getTotalElements(), productSlice, isAuthenticated + areaCode, keyword, checkInDate, checkOutDate, priceRange, + productSlice.getTotalElements(), productSlice, isAuthenticated ); return new SliceImpl<>( - Collections.singletonList(searchProductResponse), - pageable, - productSlice.hasNext() + Collections.singletonList(searchProductResponse), + pageable, + productSlice.hasNext() ); } @Transactional(readOnly = true) public Slice getProductsByAreaCode( - AreaCode areaCode, LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, PrincipalDetails principalDetails + AreaCode areaCode, LocalDate cursorCheckInDate, Long cursorId, Pageable pageable, + PrincipalDetails principalDetails ) { Long userId = (principalDetails != null) ? principalDetails.getUserId() : null; boolean isAuthenticated = (userId != null); CustomSlice productSlice = productRepository.getProductsByAreaCode( - areaCode, cursorCheckInDate, cursorId, pageable, userId + areaCode, cursorCheckInDate, cursorId, pageable, userId ); RegionProductResponse regionProductResponse = RegionProductResponse.fromEntity( - productSlice.getTotalElements(), productSlice, isAuthenticated + productSlice.getTotalElements(), productSlice, isAuthenticated ); return new SliceImpl<>( - Collections.singletonList(regionProductResponse), - pageable, - productSlice.hasNext() + Collections.singletonList(regionProductResponse), + pageable, + productSlice.hasNext() ); } @@ -103,8 +112,8 @@ public List getAllReservations(Long yaUserId) { String getUrl = buildReservationUrl(RESERVATIONS_ENDPOINT, yaUserId); List yanoljaProductResponses = restTemplateService.getList( - getUrl, - YanoljaProductResponse[].class + getUrl, + YanoljaProductResponse[].class ); List reservationResponses = new ArrayList<>(); @@ -118,7 +127,8 @@ public List getAllReservations(Long yaUserId) { return reservationResponses; } - private static ReservationResponse getReservationResponse(Optional product, YanoljaProductResponse yanoljaProductResponse) { + private static ReservationResponse getReservationResponse(Optional product, + YanoljaProductResponse yanoljaProductResponse) { if (product.isPresent()) { return ReservationResponse.from(yanoljaProductResponse, REGISTERED); } @@ -129,7 +139,7 @@ private static ReservationResponse getReservationResponse(Optional prod // 3. 상품 생성, 조회, 수정, 삭제 관련 메서드 @Transactional public ProductResponse createProduct( - ProductRequest productRequest, Long reservationId, PrincipalDetails principalDetails + ProductRequest productRequest, Long reservationId, PrincipalDetails principalDetails ) { if (productRepository.existsByReservationId(reservationId)) { throw new CustomException(PRODUCT_ALREADY_EXISTS); @@ -138,14 +148,24 @@ public ProductResponse createProduct( String getUrl = buildReservationUrl(RESERVATION_ENDPOINT, reservationId); ReservationDetailsResponse reservationDetailsResponse = restTemplateService.get( - getUrl, - ReservationDetailsResponse.class + getUrl, + ReservationDetailsResponse.class ).orElseThrow(() -> new CustomException(RESERVATION_NOT_FOUND)); - Product product = productRequest.toEntity(reservationDetailsResponse, principalDetails.getUserId()); + Product product = productRequest.toEntity(reservationDetailsResponse, + principalDetails.getUserId()); Product savedProduct = productRepository.save(product); - redisService.addZScore(AUTOCOMPLETE_KEY, product.getAccommodationName(), INITIAL_RANKING_SCORE); + redisService.addZScore(AUTOCOMPLETE_KEY, product.getAccommodationName(), + INITIAL_RANKING_SCORE); + + //해당 지역 찜한 회원들에게 알림 전송 + AreaCode areaCode = product.getAreaCode(); + List userList = wishRegionService.findUserIdByRegion(areaCode); + for (Long userId : userList) { + alertService.createAlert(userId, + "관심있던 '" + areaCode.getAreaName() + "'지역에 새로운 상품이 등록되었습니다! 사라지기 전에 확인해보세요!"); + } return ProductResponse.fromEntity(savedProduct); } @@ -171,7 +191,7 @@ public Long deleteProduct(Long productId) { // 4. 기타 유틸 메서드 public Product getProduct(Long productId) { return productRepository.findById(productId) - .orElseThrow(() -> new CustomException(PRODUCT_NOT_FOUND)); + .orElseThrow(() -> new CustomException(PRODUCT_NOT_FOUND)); } Product getProductWithWishProducts(Long productId, Long userId) { @@ -184,32 +204,33 @@ public Product save(Product product) { private String buildReservationUrl(String endpoint, Long pathVariable) { return UriComponentsBuilder - .fromUriString(DISTRIBUTE_BASE_URL) - .path(endpoint) - .buildAndExpand(pathVariable) - .encode(StandardCharsets.UTF_8) - .toUriString(); + .fromUriString(DISTRIBUTE_BASE_URL) + .path(endpoint) + .buildAndExpand(pathVariable) + .encode(StandardCharsets.UTF_8) + .toUriString(); } - public String generateOrRetrieveAnonymousKey(HttpServletRequest request, HttpServletResponse response) { + public String generateOrRetrieveAnonymousKey(HttpServletRequest request, + HttpServletResponse response) { Cookie[] cookies = request.getCookies(); if (cookies != null) { String anonymousKey = Arrays.stream(cookies) - .filter(cookie -> "AnonymousKey".equals(cookie.getName())) - .map(Cookie::getValue) - .findFirst() - .orElse(null); + .filter(cookie -> "AnonymousKey".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); if (anonymousKey == null) { anonymousKey = UUID.randomUUID().toString(); ResponseCookie cookie = ResponseCookie.from("AnonymousKey", anonymousKey) - .domain(".golden-ticket.site") - .httpOnly(false) - .secure(true) - .path("/") - .sameSite("None") - .build(); + .domain(".golden-ticket.site") + .httpOnly(false) + .secure(true) + .path("/") + .sameSite("None") + .build(); response.addHeader("Set-Cookie", cookie.toString()); } @@ -218,12 +239,12 @@ public String generateOrRetrieveAnonymousKey(HttpServletRequest request, HttpSer } else { String anonymousKey = UUID.randomUUID().toString(); ResponseCookie cookie = ResponseCookie.from("AnonymousKey", anonymousKey) - .domain(".golden-ticket.site") - .httpOnly(false) - .secure(true) - .path("/") - .sameSite("None") - .build(); + .domain(".golden-ticket.site") + .httpOnly(false) + .secure(true) + .path("/") + .sameSite("None") + .build(); response.addHeader("Set-Cookie", cookie.toString()); @@ -240,14 +261,18 @@ public void updateProductViewCount(String userKey, String productKey) { redisService.leftPush(viewProductKey, productKey); Double currentViewCount = redisService.getZScore(VIEW_RANKING_KEY, productKey); - Double updateViewCount = (currentViewCount != null) ? currentViewCount + 1 : SCORE_INCREMENT_AMOUNT; + Double updateViewCount = + (currentViewCount != null) ? currentViewCount + 1 : SCORE_INCREMENT_AMOUNT; redisService.addZScore(VIEW_RANKING_KEY, productKey, updateViewCount); } } public void updateAutocompleteCount(String autocompleteKey, String accommodationName) { - Double currentAutocompleteCount = redisService.getZScore(autocompleteKey, accommodationName); - Double updateAutocompleteCount = (currentAutocompleteCount != null) ? SCORE_INCREMENT_AMOUNT + 1 : SCORE_INCREMENT_AMOUNT; + Double currentAutocompleteCount = redisService.getZScore(autocompleteKey, + accommodationName); + Double updateAutocompleteCount = + (currentAutocompleteCount != null) ? SCORE_INCREMENT_AMOUNT + 1 + : SCORE_INCREMENT_AMOUNT; redisService.addZScore(autocompleteKey, accommodationName, updateAutocompleteCount); } @@ -256,26 +281,33 @@ public HomeProductResponse getHomeProduct(PrincipalDetails principalDetails) { boolean isAuthenticated = (userId != null); - Pageable pageable = PageRequest.of(PaginationConstants.DEFAULT_PAGE, PaginationConstants.MIN_PAGE_SIZE); + Pageable pageable = PageRequest.of(PaginationConstants.DEFAULT_PAGE, + PaginationConstants.MIN_PAGE_SIZE); - List goldenPriceTop5 = getProductResponseList(productRepository.findTop5ByGoldenPriceAsc(userId, pageable), isAuthenticated); - List viewCountTop5 = getProductResponseList(productRepository.findTop5ByViewCountDesc(userId, pageable), isAuthenticated); - List recentRegisteredTop5 = getProductResponseList(productRepository.findTop5ByIdDesc(userId, pageable), isAuthenticated); - List dayUseTop5 = getProductResponseList(productRepository.findTop5DayUseProductsCheckInDateAsc(userId, pageable), isAuthenticated); + List goldenPriceTop5 = getProductResponseList( + productRepository.findTop5ByGoldenPriceAsc(userId, pageable), isAuthenticated); + List viewCountTop5 = getProductResponseList( + productRepository.findTop5ByViewCountDesc(userId, pageable), isAuthenticated); + List recentRegisteredTop5 = getProductResponseList( + productRepository.findTop5ByIdDesc(userId, pageable), isAuthenticated); + List dayUseTop5 = getProductResponseList( + productRepository.findTop5DayUseProductsCheckInDateAsc(userId, pageable), + isAuthenticated); return new HomeProductResponse( - goldenPriceTop5, - viewCountTop5, - recentRegisteredTop5, - dayUseTop5 + goldenPriceTop5, + viewCountTop5, + recentRegisteredTop5, + dayUseTop5 ); } - private List getProductResponseList(List productList, boolean isAuthenticated) { + private List getProductResponseList(List productList, + boolean isAuthenticated) { return productList.stream() - .map( - product -> WishedProductResponse.fromEntity(product, isAuthenticated)) - .collect(Collectors.toList()); + .map( + product -> WishedProductResponse.fromEntity(product, isAuthenticated)) + .collect(Collectors.toList()); } public List findProductListByUserId(Long userId) { @@ -287,7 +319,8 @@ public void updateProductForNego(Product product) { productRepository.save(product); } - public List findByProductStatusInAndUserId(List productStatusList, Long userId) { + public List findByProductStatusInAndUserId(List productStatusList, + Long userId) { return productRepository.findByProductStatusInAndUserId(productStatusList, userId); } diff --git a/src/main/java/site/goldenticket/domain/user/wish/repository/WishRegionRepository.java b/src/main/java/site/goldenticket/domain/user/wish/repository/WishRegionRepository.java index 51396c92..453e88a4 100644 --- a/src/main/java/site/goldenticket/domain/user/wish/repository/WishRegionRepository.java +++ b/src/main/java/site/goldenticket/domain/user/wish/repository/WishRegionRepository.java @@ -1,6 +1,7 @@ package site.goldenticket.domain.user.wish.repository; import org.springframework.data.jpa.repository.JpaRepository; +import site.goldenticket.domain.product.constants.AreaCode; import site.goldenticket.domain.user.wish.entity.WishRegion; import java.util.List; @@ -8,4 +9,5 @@ public interface WishRegionRepository extends JpaRepository { List findByUserId(Long userId); + List findByRegion(AreaCode region); } diff --git a/src/main/java/site/goldenticket/domain/user/wish/service/WishRegionService.java b/src/main/java/site/goldenticket/domain/user/wish/service/WishRegionService.java index a811c54b..a617a96c 100644 --- a/src/main/java/site/goldenticket/domain/user/wish/service/WishRegionService.java +++ b/src/main/java/site/goldenticket/domain/user/wish/service/WishRegionService.java @@ -1,19 +1,21 @@ package site.goldenticket.domain.user.wish.service; +import static site.goldenticket.common.response.ErrorCode.USER_NOT_FOUND; +import static site.goldenticket.common.response.ErrorCode.WISH_REGION_OVER_MAXIMUM; + +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import site.goldenticket.common.exception.CustomException; +import site.goldenticket.domain.product.constants.AreaCode; import site.goldenticket.domain.user.entity.User; import site.goldenticket.domain.user.repository.UserRepository; -import site.goldenticket.domain.user.wish.repository.WishRegionRepository; import site.goldenticket.domain.user.wish.dto.WishRegionRegisterRequest; import site.goldenticket.domain.user.wish.entity.WishRegion; - -import java.util.List; - -import static site.goldenticket.common.response.ErrorCode.*; +import site.goldenticket.domain.user.wish.repository.WishRegionRepository; @Slf4j @Service @@ -27,7 +29,8 @@ public class WishRegionService { private final UserRepository userRepository; @Transactional - public void registerWishRegion(Long userId, WishRegionRegisterRequest wishRegionRegisterRequest) { + public void registerWishRegion(Long userId, + WishRegionRegisterRequest wishRegionRegisterRequest) { User user = findByIdWithWishRegion(userId); log.info("User ID [{}] Register Regions = {}", userId, wishRegionRegisterRequest.regions()); @@ -45,6 +48,20 @@ public List findWishRegion(Long userId) { private User findByIdWithWishRegion(Long userId) { return userRepository.findByIdFetchWishRegion(userId) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + } + + /*** + * 특정 지역을 관심 지역으로 등록한 회원 ID 목록 조회 + * @param areaCode 관심 지역 + * @return 회원 ID List + */ + public List findUserIdByRegion(AreaCode areaCode) { + List wishRegionList = wishRegionRepository.findByRegion(areaCode); + List userIdList = new ArrayList<>(); + for (WishRegion wishRegion : wishRegionList) { + userIdList.add(wishRegion.getUser().getId()); + } + return userIdList; } } From 291a1ad59af6dcf5cf2fc9b485be6fff680f683d Mon Sep 17 00:00:00 2001 From: dldmldlsy Date: Wed, 24 Jan 2024 11:37:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=EA=B4=80=EC=8B=AC=20=EC=A7=80?= =?UTF-8?q?=EC=97=AD=20=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=84=EC=86=A1=20-=20=EC=83=81=ED=92=88=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=EC=97=90=EB=8F=84=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=A0=84=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상품 수정 시, 지역을 수정했다면, 바뀐 지역에 대해서 찜한 회원들에게 알림 전송 - 관심지역 알림 전송하는 메서드 별도 구현하여 활용 --- .../domain/alert/service/AlertService.java | 11 +++++++++++ .../domain/product/service/ProductService.java | 14 +++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java index b4fdd56e..caed43be 100644 --- a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java +++ b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java @@ -13,6 +13,8 @@ import site.goldenticket.domain.alert.dto.AlertUnSeenResponse; import site.goldenticket.domain.alert.entity.Alert; import site.goldenticket.domain.alert.repository.AlertRepository; +import site.goldenticket.domain.product.constants.AreaCode; +import site.goldenticket.domain.user.wish.service.WishRegionService; @Service @Transactional @@ -20,6 +22,7 @@ public class AlertService { private final AlertRepository alertRepository; + private final WishRegionService wishRegionService; public AlertResponse createAlertForTest(AlertRequest alertRequest) { Alert alert = Alert.builder() @@ -70,4 +73,12 @@ public AlertListResponse getAlertListByUserId(Long userId) { Comparator.comparing(AlertResponse::createdAt).reversed()); return AlertListResponse.builder().alertResponses(alertResponses).build(); } + + public void createAlertOfWishRegion(AreaCode areaCode) { + List userList = wishRegionService.findUserIdByRegion(areaCode); + for (Long userId : userList) { + createAlert(userId, + "관심있던 '" + areaCode.getAreaName() + "'지역에 새로운 상품이 등록되었습니다! 사라지기 전에 확인해보세요!"); + } + } } diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index 385ea486..98291470 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -160,12 +160,7 @@ public ProductResponse createProduct( INITIAL_RANKING_SCORE); //해당 지역 찜한 회원들에게 알림 전송 - AreaCode areaCode = product.getAreaCode(); - List userList = wishRegionService.findUserIdByRegion(areaCode); - for (Long userId : userList) { - alertService.createAlert(userId, - "관심있던 '" + areaCode.getAreaName() + "'지역에 새로운 상품이 등록되었습니다! 사라지기 전에 확인해보세요!"); - } + alertService.createAlertOfWishRegion(product.getAreaCode()); return ProductResponse.fromEntity(savedProduct); } @@ -174,8 +169,13 @@ public ProductResponse createProduct( public ProductResponse updateProduct(ProductRequest productRequest, Long productId) { Product product = getProduct(productId); product.update(productRequest.goldenPrice(), productRequest.content()); + AreaCode pastAreaCode = product.getAreaCode(); Product updatedProduct = productRepository.save(product); - + AreaCode nowAreaCode = updatedProduct.getAreaCode(); + //해당 지역 찜한 회원들에게 알림 전송 + if(!nowAreaCode.equals(pastAreaCode)) { + alertService.createAlertOfWishRegion(nowAreaCode); + } return ProductResponse.fromEntity(updatedProduct); } From 47d5df8abd69d76fd71555c78662c1702fc43fc0 Mon Sep 17 00:00:00 2001 From: dldmldlsy Date: Wed, 24 Jan 2024 13:23:53 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20-=20=EA=B4=80=EC=8B=AC=EC=A7=80=EC=97=AD,=20?= =?UTF-8?q?=EA=B4=80=EC=8B=AC=EC=88=99=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관심 지역에 상품 등록된 경우 - 관심 지역으로 상품이 수정된 경우 - 양도 취소로 인해 관심 상품이 판매중으로 바뀐 경우 --- .../domain/alert/service/AlertService.java | 11 +++++++ .../domain/nego/service/NegoServiceImpl.java | 8 +++++ .../repository/WishProductRepository.java | 1 + .../wish/service/WishProductService.java | 30 ++++++++++++++----- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java index caed43be..0557e400 100644 --- a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java +++ b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java @@ -14,6 +14,7 @@ import site.goldenticket.domain.alert.entity.Alert; import site.goldenticket.domain.alert.repository.AlertRepository; import site.goldenticket.domain.product.constants.AreaCode; +import site.goldenticket.domain.product.wish.service.WishProductService; import site.goldenticket.domain.user.wish.service.WishRegionService; @Service @@ -23,6 +24,7 @@ public class AlertService { private final AlertRepository alertRepository; private final WishRegionService wishRegionService; + private final WishProductService wishProductService; public AlertResponse createAlertForTest(AlertRequest alertRequest) { Alert alert = Alert.builder() @@ -81,4 +83,13 @@ public void createAlertOfWishRegion(AreaCode areaCode) { "관심있던 '" + areaCode.getAreaName() + "'지역에 새로운 상품이 등록되었습니다! 사라지기 전에 확인해보세요!"); } } + + public void createAlertOfWishProductToSelling(Long productId, String accommodationName, + String roomName) { + List userList = wishProductService.findUserIdListByProductId(productId); + for (Long userId : userList) { + createAlert(userId, "찜한 ‘" + accommodationName + "(" + roomName + + ")' 상품이 판매중으로 변경되어 다시 구매가 가능합니다. 빠르게 거래를 진행해주세요!"); + } + } } diff --git a/src/main/java/site/goldenticket/domain/nego/service/NegoServiceImpl.java b/src/main/java/site/goldenticket/domain/nego/service/NegoServiceImpl.java index 6314f4ed..891dbf57 100644 --- a/src/main/java/site/goldenticket/domain/nego/service/NegoServiceImpl.java +++ b/src/main/java/site/goldenticket/domain/nego/service/NegoServiceImpl.java @@ -276,6 +276,10 @@ public NegoResponse denyHandoverProduct(Long productId, PrincipalDetails princip //판매자에게 양도 취소 알림 전송 alertService.createAlert(product.getUserId(), "양도가 취소되었습니다. 구매자에게 결제 금액이 100% 환불됩니다."); + //해당 상품 찜한 회원들에게 알림 전송 + if(product.getProductStatus().equals(ProductStatus.SELLING)) { + alertService.createAlertOfWishProductToSelling(productId, product.getAccommodationName(), product.getRoomName()); + } } if (transferPendingNego.isPresent()) { @@ -298,6 +302,10 @@ public NegoResponse denyHandoverProduct(Long productId, PrincipalDetails princip //판매자에게 양도 취소 알림 전송 alertService.createAlert(product.getUserId(), "양도가 취소되었습니다. 구매자에게 결제 금액이 100% 환불됩니다."); + //해당 상품 찜한 회원들에게 알림 전송 + if(product.getProductStatus().equals(ProductStatus.SELLING)) { + alertService.createAlertOfWishProductToSelling(productId, product.getAccommodationName(), product.getRoomName()); + } return NegoResponse.fromEntity(nego); } diff --git a/src/main/java/site/goldenticket/domain/product/wish/repository/WishProductRepository.java b/src/main/java/site/goldenticket/domain/product/wish/repository/WishProductRepository.java index 09fb6c5f..74c5729c 100644 --- a/src/main/java/site/goldenticket/domain/product/wish/repository/WishProductRepository.java +++ b/src/main/java/site/goldenticket/domain/product/wish/repository/WishProductRepository.java @@ -19,4 +19,5 @@ public interface WishProductRepository extends JpaRepository List findByUserIdWithProduct(@Param("userId") Long userId); Optional findByUserIdAndProductId(Long userId, Long product_id); + List findByProductId(Long productId); } diff --git a/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java b/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java index 4d596378..5b12bf5a 100644 --- a/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java +++ b/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java @@ -1,5 +1,9 @@ package site.goldenticket.domain.product.wish.service; +import static site.goldenticket.common.response.ErrorCode.WISH_PRODUCT_NOT_FOUND; + +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -10,10 +14,6 @@ import site.goldenticket.domain.product.wish.entity.WishProduct; import site.goldenticket.domain.product.wish.repository.WishProductRepository; -import java.util.List; - -import static site.goldenticket.common.response.ErrorCode.WISH_PRODUCT_NOT_FOUND; - @Slf4j @Service @Transactional(readOnly = true) @@ -48,13 +48,27 @@ public void deleteWishProduct(Long userId, Long productId) { private WishProduct findByUserIdAndProductId(Long userId, Long productId) { return wishProductRepository.findByUserIdAndProductId(userId, productId) - .orElseThrow(() -> new CustomException(WISH_PRODUCT_NOT_FOUND)); + .orElseThrow(() -> new CustomException(WISH_PRODUCT_NOT_FOUND)); } private WishProduct createWishProduct(Long userId, Product product) { return WishProduct.builder() - .userId(userId) - .product(product) - .build(); + .userId(userId) + .product(product) + .build(); + } + + /*** + * 특정 상품을 관심 상품으로 등록한 회원 ID 목록 조회 + * @param productId 관심 상품 ID + * @return 회원 ID List + */ + public List findUserIdListByProductId(Long productId) { + List wishProductList = wishProductRepository.findByProductId(productId); + List userIdList = new ArrayList<>(); + for (WishProduct wishProduct : wishProductList) { + userIdList.add(wishProduct.getUserId()); + } + return userIdList; } } From ae062f867f449f87fce6f7db3f3a2d9a2f095d7d Mon Sep 17 00:00:00 2001 From: dldmldlsy Date: Wed, 24 Jan 2024 21:38:37 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EC=88=9C=ED=99=98=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관심 숙소 알림 전송 로직에서 순환 참조 발생 -> 직접 접근을 통해 해결 --- .../alert/repository/AlertRepository.java | 1 - .../domain/alert/service/AlertService.java | 21 ++++++++++++++++--- .../product/service/ProductService.java | 2 -- .../wish/service/WishProductService.java | 15 ------------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java b/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java index 5a828901..63ee1a18 100644 --- a/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java +++ b/src/main/java/site/goldenticket/domain/alert/repository/AlertRepository.java @@ -3,7 +3,6 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import site.goldenticket.domain.alert.entity.Alert; -import site.goldenticket.domain.product.constants.AreaCode; public interface AlertRepository extends JpaRepository { diff --git a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java index 0557e400..7bd4d70c 100644 --- a/src/main/java/site/goldenticket/domain/alert/service/AlertService.java +++ b/src/main/java/site/goldenticket/domain/alert/service/AlertService.java @@ -14,7 +14,8 @@ import site.goldenticket.domain.alert.entity.Alert; import site.goldenticket.domain.alert.repository.AlertRepository; import site.goldenticket.domain.product.constants.AreaCode; -import site.goldenticket.domain.product.wish.service.WishProductService; +import site.goldenticket.domain.product.wish.entity.WishProduct; +import site.goldenticket.domain.product.wish.repository.WishProductRepository; import site.goldenticket.domain.user.wish.service.WishRegionService; @Service @@ -24,7 +25,7 @@ public class AlertService { private final AlertRepository alertRepository; private final WishRegionService wishRegionService; - private final WishProductService wishProductService; + private final WishProductRepository wishProductRepository; public AlertResponse createAlertForTest(AlertRequest alertRequest) { Alert alert = Alert.builder() @@ -86,10 +87,24 @@ public void createAlertOfWishRegion(AreaCode areaCode) { public void createAlertOfWishProductToSelling(Long productId, String accommodationName, String roomName) { - List userList = wishProductService.findUserIdListByProductId(productId); + List userList = findUserIdListByProductId(productId); for (Long userId : userList) { createAlert(userId, "찜한 ‘" + accommodationName + "(" + roomName + ")' 상품이 판매중으로 변경되어 다시 구매가 가능합니다. 빠르게 거래를 진행해주세요!"); } } + + /*** + * 특정 상품을 관심 상품으로 등록한 회원 ID 목록 조회 + * @param productId 관심 상품 ID + * @return 회원 ID List + */ + public List findUserIdListByProductId(Long productId) { + List wishProductList = wishProductRepository.findByProductId(productId); + List userIdList = new ArrayList<>(); + for (WishProduct wishProduct : wishProductList) { + userIdList.add(wishProduct.getUserId()); + } + return userIdList; + } } diff --git a/src/main/java/site/goldenticket/domain/product/service/ProductService.java b/src/main/java/site/goldenticket/domain/product/service/ProductService.java index 98291470..020b94c0 100644 --- a/src/main/java/site/goldenticket/domain/product/service/ProductService.java +++ b/src/main/java/site/goldenticket/domain/product/service/ProductService.java @@ -26,7 +26,6 @@ import site.goldenticket.domain.product.repository.CustomSlice; import site.goldenticket.domain.product.repository.ProductRepository; import site.goldenticket.domain.security.PrincipalDetails; -import site.goldenticket.domain.user.wish.service.WishRegionService; import site.goldenticket.dummy.reservation.dto.ReservationDetailsResponse; import site.goldenticket.dummy.reservation.dto.YanoljaProductResponse; @@ -51,7 +50,6 @@ public class ProductService { private final RedisService redisService; private final ProductRepository productRepository; private final AlertService alertService; - private final WishRegionService wishRegionService; // 1. 키워드 검색 및 지역 검색 메서드 @Transactional(readOnly = true) diff --git a/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java b/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java index 5b12bf5a..2da9dec3 100644 --- a/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java +++ b/src/main/java/site/goldenticket/domain/product/wish/service/WishProductService.java @@ -2,7 +2,6 @@ import static site.goldenticket.common.response.ErrorCode.WISH_PRODUCT_NOT_FOUND; -import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -57,18 +56,4 @@ private WishProduct createWishProduct(Long userId, Product product) { .product(product) .build(); } - - /*** - * 특정 상품을 관심 상품으로 등록한 회원 ID 목록 조회 - * @param productId 관심 상품 ID - * @return 회원 ID List - */ - public List findUserIdListByProductId(Long productId) { - List wishProductList = wishProductRepository.findByProductId(productId); - List userIdList = new ArrayList<>(); - for (WishProduct wishProduct : wishProductList) { - userIdList.add(wishProduct.getUserId()); - } - return userIdList; - } } From 8eac8f0d822210862b9a0d2770b8741e66018770 Mon Sep 17 00:00:00 2001 From: dldmldlsy Date: Wed, 24 Jan 2024 21:49:12 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EA=B4=80=EC=8B=AC=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=9E=AC=ED=8C=90=EB=A7=A4=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타임오버로 재판매되는 관심상품에 대해 해당 상품을 찜한 회원들에게 재판매 알림 전송 --- .../nego/service/NegoSchedulerService.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/site/goldenticket/domain/nego/service/NegoSchedulerService.java b/src/main/java/site/goldenticket/domain/nego/service/NegoSchedulerService.java index 73ddec19..d500afed 100644 --- a/src/main/java/site/goldenticket/domain/nego/service/NegoSchedulerService.java +++ b/src/main/java/site/goldenticket/domain/nego/service/NegoSchedulerService.java @@ -49,13 +49,19 @@ public void changeStatus() { nego.setStatus(NEGOTIATION_TIMEOUT); nego.setUpdatedAt(currentTime); + negoRepository.save(nego); + //판매자에게 타임오버 알림 전송 alertService.createAlert(product.getUserId(), - "구매자가 20분 이내에 결제를 완료하지 않아 거래가 이루어지지 않았습니다."); + "구매자가 20분 이내에 결제를 완료하지 않아 거래가 이루어지지 않았습니다."); //구매자에게 타임오버 알림 전송 alertService.createAlert(nego.getUser().getId(), - "20분이 초과되었습니다. 아직 구매를 원하신다면, 재결제 버튼을 눌러 결제해주세요."); - negoRepository.save(nego); + "20분이 초과되었습니다. 아직 구매를 원하신다면, 재결제 버튼을 눌러 결제해주세요."); + //해당 상품 찜한 회원들에게 알림 전송 + if (product.getProductStatus().equals(ProductStatus.SELLING)) { + alertService.createAlertOfWishProductToSelling(product.getId(), + product.getAccommodationName(), product.getRoomName()); + } } } //상품 상태 판매중 @@ -65,7 +71,7 @@ public void changeStatus() { LocalDateTime updatedAt = transferOrder.getUpdatedAt(); User user = userRepository.findById(product.getUserId()) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); checkAccountAndThrowException(user); @@ -85,28 +91,29 @@ public void changeStatus() { // 구매자에게 양도 완료 알림 전송 alertService.createAlert(transferNego.getUser().getId(), - "'" + product.getAccommodationName() + "(" + product.getRoomName() - + ")'상품 양도가 완료되었습니다. " - + "양도 완료에 따른 체크인 정보는 '마이페이지 > 구매내역 > 구매 완료'에서 확인하실 수 있습니다."); + "'" + product.getAccommodationName() + "(" + product.getRoomName() + + ")'상품 양도가 완료되었습니다. " + + "양도 완료에 따른 체크인 정보는 '마이페이지 > 구매내역 > 구매 완료'에서 확인하실 수 있습니다."); // 판매자에게 정산 요청 알림 전송 alertService.createAlert(product.getUserId(), - "'" + product.getAccommodationName() + "(" + product.getRoomName() - + ")'상품 양도가 완료되었습니다. 영업일 1일 이내 등록한 계좌 정보로 정산 금액이 입금됩니다." - + "원활한 정산 진행을 위해 '마이페이지 - 나의 계좌'정보를 다시 한번 확인해주세요."); + "'" + product.getAccommodationName() + "(" + product.getRoomName() + + ")'상품 양도가 완료되었습니다. 영업일 1일 이내 등록한 계좌 정보로 정산 금액이 입금됩니다." + + "원활한 정산 진행을 위해 '마이페이지 - 나의 계좌'정보를 다시 한번 확인해주세요."); // 판매자에게 계좌 등록 알림 전송 if (user != null && user.getAccountNumber() == null) { alertService.createAlert(product.getUserId(), - "'" + product.getAccommodationName() + "(" + product.getRoomName() - + ")'상품에 대한 원활한 정산을 위해 '마이페이지 > 내 계좌'에서 입금받으실 계좌를 등록해주세요."); + "'" + product.getAccommodationName() + "(" + product.getRoomName() + + ")'상품에 대한 원활한 정산을 위해 '마이페이지 > 내 계좌'에서 입금받으실 계좌를 등록해주세요."); } } negoRepository.saveAll(transferNegos); } } // 20분 뒤 자동양도 } + private void checkAccountAndThrowException(User user) { if (user.getAccountNumber() == null) { throw new CustomException("등록된 계좌가 없습니다.", ErrorCode.NO_REGISTERED_ACCOUNT);