diff --git a/src/main/java/com/salemale/domain/item/controller/ItemController.java b/src/main/java/com/salemale/domain/item/controller/ItemController.java index afece6c..817fc65 100644 --- a/src/main/java/com/salemale/domain/item/controller/ItemController.java +++ b/src/main/java/com/salemale/domain/item/controller/ItemController.java @@ -185,6 +185,15 @@ public ResponseEntity> getAuctions( // Pageable 객체 생성 Pageable pageable = PageRequest.of(page, size); + // 로그인 안 했으면 null + // 로그인 했으면 userId + Long loginUserId = null; + try { + loginUserId = currentUserProvider.getCurrentUserId(request); + } catch (Exception e) { + // 비로그인 요청이면 그대로 null 유지 + } + // 추가: RECOMMENDED 상태일 때는 별도 처리 if (status == AuctionStatus.RECOMMENDED) { Long userId = currentUserProvider.getCurrentUserId(request); @@ -193,7 +202,7 @@ public ResponseEntity> getAuctions( } // 서비스 호출 - AuctionListResponse response = itemService.getAuctionList(status, categories, minPrice, maxPrice, sort, pageable); + AuctionListResponse response = itemService.getAuctionList(status, categories, minPrice, maxPrice, sort, pageable, loginUserId); return ResponseEntity.ok(ApiResponse.onSuccess(response)); } diff --git a/src/main/java/com/salemale/domain/item/converter/ItemConverter.java b/src/main/java/com/salemale/domain/item/converter/ItemConverter.java index 1551629..cac3a39 100644 --- a/src/main/java/com/salemale/domain/item/converter/ItemConverter.java +++ b/src/main/java/com/salemale/domain/item/converter/ItemConverter.java @@ -188,6 +188,30 @@ public static AuctionListItemDTO toAuctionListItemDTO(Item item) { .build(); } + //⭐추가 메서드: 차단 여부 포함 버전 + public static AuctionListItemDTO toAuctionListItemDTO( + Item item, + boolean blockedSeller // Service에서 계산된 값 + ) { + List imageUrls = item.getImages().stream() + .map(ItemImage::getImageUrl) + .collect(Collectors.toList()); + + return AuctionListItemDTO.builder() + .itemId(item.getItemId()) + .title(item.getTitle()) + .imageUrls(imageUrls) + .currentPrice(item.getCurrentPrice()) + .bidderCount(item.getBidCount()) + .endTime(item.getEndTime()) + .viewCount(item.getViewCount()) + .itemStatus(item.getItemStatus().name()) + .startPrice(item.getStartPrice()) + .createdAt(item.getCreatedAt()) + .blockedSeller(blockedSeller) // 차단 여부 + .build(); + } + /** * Item → MyAuctionItemDTO 변환 * @param item 상품 엔티티 diff --git a/src/main/java/com/salemale/domain/item/dto/response/AuctionListItemDTO.java b/src/main/java/com/salemale/domain/item/dto/response/AuctionListItemDTO.java index dc97c1b..3441e36 100644 --- a/src/main/java/com/salemale/domain/item/dto/response/AuctionListItemDTO.java +++ b/src/main/java/com/salemale/domain/item/dto/response/AuctionListItemDTO.java @@ -48,4 +48,8 @@ public class AuctionListItemDTO { @Schema(description = "경매 시작 날짜") private LocalDateTime createdAt; // 생성일(경매 시작날짜) 추가 + + // 로그인 사용자가 이 판매자를 차단했는지 여부 + @Schema(description = "차단한 판매자의 상품인지 여부", example = "false") + private boolean blockedSeller; } diff --git a/src/main/java/com/salemale/domain/item/repository/ItemRepository.java b/src/main/java/com/salemale/domain/item/repository/ItemRepository.java index 4d851a6..4d3050b 100644 --- a/src/main/java/com/salemale/domain/item/repository/ItemRepository.java +++ b/src/main/java/com/salemale/domain/item/repository/ItemRepository.java @@ -59,13 +59,6 @@ List findByEndTimeBeforeAndItemStatus( JOIN region r ON i.region_id = r.region_id WHERE i.item_type = 'AUCTION' AND i.item_status = CAST(:status AS varchar) - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( 6371 * acos( LEAST(1, GREATEST(-1, @@ -83,13 +76,6 @@ SELECT count(1) JOIN region r ON i.region_id = r.region_id WHERE i.item_type = 'AUCTION' AND i.item_status = CAST(:status AS varchar) - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( 6371 * acos( LEAST(1, GREATEST(-1, @@ -106,7 +92,6 @@ Page findNearbyItems( @Param("lat") double centerLat, @Param("lon") double centerLon, @Param("distanceKm") double distanceKm, - @Param("me") Long me, Pageable pageable ); @@ -117,13 +102,6 @@ Page findNearbyItems( SELECT i FROM Item i WHERE i.itemType = com.salemale.global.common.enums.ItemType.AUCTION AND i.itemStatus = :status - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( LOWER(i.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(i.name) LIKE LOWER(CONCAT('%', :keyword, '%')) @@ -133,7 +111,6 @@ OR LOWER(i.name) LIKE LOWER(CONCAT('%', :keyword, '%')) Page searchItemsByKeyword( @Param("status") com.salemale.global.common.enums.ItemStatus status, @Param("keyword") String keyword, - @Param("me") Long me, Pageable pageable ); @@ -147,13 +124,6 @@ Page searchItemsByKeyword( SELECT i FROM Item i WHERE i.itemType = com.salemale.global.common.enums.ItemType.AUCTION AND i.itemStatus = :status - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( LOWER(i.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(i.name) LIKE LOWER(CONCAT('%', :keyword, '%')) @@ -176,7 +146,6 @@ Page searchItemsByKeywordWithFilters( @Param("isPopular") boolean isPopular, @Param("threeDaysAgo") LocalDateTime threeDaysAgo, @Param("now") LocalDateTime now, - @Param("me") Long me, Pageable pageable ); @@ -190,13 +159,6 @@ Page searchItemsByKeywordWithFilters( SELECT i FROM Item i WHERE i.itemType = com.salemale.global.common.enums.ItemType.AUCTION AND i.itemStatus = :status - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND (:categories IS NULL OR i.category IN :categories) AND (:minPrice IS NULL OR i.currentPrice >= :minPrice) AND (:maxPrice IS NULL OR i.currentPrice <= :maxPrice) @@ -214,7 +176,6 @@ Page searchItemsByFiltersOnly( @Param("isPopular") boolean isPopular, @Param("threeDaysAgo") LocalDateTime threeDaysAgo, @Param("now") LocalDateTime now, - @Param("me") Long me, Pageable pageable ); @@ -227,13 +188,6 @@ Page searchItemsByFiltersOnly( JOIN region r ON i.region_id = r.region_id WHERE i.item_type = 'AUCTION' AND i.item_status = CAST(:status AS varchar) - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( LOWER(i.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(i.name) LIKE LOWER(CONCAT('%', :keyword, '%')) @@ -255,13 +209,6 @@ SELECT count(1) JOIN region r ON i.region_id = r.region_id WHERE i.item_type = 'AUCTION' AND i.item_status = CAST(:status AS varchar) - AND ( - :me IS NULL OR i.seller NOT IN ( - SELECT bl.blocked - FROM BlockList bl - WHERE bl.blocker.id = :me - ) - ) AND ( LOWER(i.title) LIKE LOWER(CONCAT('%', :keyword, '%')) OR LOWER(i.name) LIKE LOWER(CONCAT('%', :keyword, '%')) @@ -283,7 +230,6 @@ Page findNearbyItemsByKeyword( @Param("lat") double centerLat, @Param("lon") double centerLon, @Param("distanceKm") double distanceKm, - @Param("me") Long me, Pageable pageable ); Long countBySeller(User seller); diff --git a/src/main/java/com/salemale/domain/item/service/ItemService.java b/src/main/java/com/salemale/domain/item/service/ItemService.java index 7faa844..6f2e594 100644 --- a/src/main/java/com/salemale/domain/item/service/ItemService.java +++ b/src/main/java/com/salemale/domain/item/service/ItemService.java @@ -19,6 +19,7 @@ import com.salemale.domain.region.entity.Region; import com.salemale.domain.s3.service.S3Service; import com.salemale.domain.user.entity.User; +import com.salemale.domain.user.repository.BlockListRepository; import com.salemale.domain.user.repository.UserRegionRepository; import com.salemale.domain.user.repository.UserRepository; import com.salemale.global.common.enums.*; @@ -35,6 +36,8 @@ import java.time.format.DateTimeParseException; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.IntStream; @Slf4j @@ -52,6 +55,7 @@ public class ItemService { private final ImageService imageService; private final RecommendationService recommendationService; private final ReviewRepository reviewRepository; + private final BlockListRepository blockListRepository; //차단 조회용 //찜하기 @Transactional @@ -365,16 +369,32 @@ public AuctionListResponse getAuctionList( Integer minPrice, Integer maxPrice, AuctionSortType sortType, - Pageable pageable + Pageable pageable, + Long loginUserId // 로그인 사용자 ID ) { // 1. QueryDSL로 동적 쿼리 실행 (이미 DB에서 정렬됨) Page itemPage = itemRepository.findAuctionList( status, categories, minPrice, maxPrice, sortType, pageable ); - // 2. DTO 변환 (엔티티의 bidCount 사용) + // ⭐ 차단한 판매자 ID 목록 조회 (로그인 상태일 때만) + Set tempblockedSellerIds = Collections.emptySet(); + if (loginUserId != null) { + tempblockedSellerIds = blockListRepository.findBlockedUserIds(loginUserId) + .stream() + .collect(Collectors.toSet()); + } + + final Set blockedSellerIds = tempblockedSellerIds; + // ⭐ DTO 변환 + 차단 여부 계산 List items = itemPage.getContent().stream() - .map(item -> ItemConverter.toAuctionListItemDTO(item)) + .map(item -> { + boolean blockedSeller = + loginUserId != null && + blockedSellerIds.contains(item.getSeller().getId()); + + return ItemConverter.toAuctionListItemDTO(item, blockedSeller); + }) .toList(); // 3. 페이징 정보와 함께 응답 DTO 생성 @@ -407,7 +427,7 @@ public AuctionListResponse getRecommendedAuctionList(Long userId, Pageable pagea if (recommendedItemIds.isEmpty()) { log.info("[추천 대체] 사용자 ID: {}, 인기 상품으로 대체", userId); return getAuctionList(AuctionStatus.POPULAR, null, null, null, - AuctionSortType.BID_COUNT_DESC, pageable); + AuctionSortType.BID_COUNT_DESC, pageable, userId); } // 3. 페이징 처리 diff --git a/src/main/java/com/salemale/domain/search/service/KeywordItemSearchServiceImpl.java b/src/main/java/com/salemale/domain/search/service/KeywordItemSearchServiceImpl.java index 80d3823..610f536 100644 --- a/src/main/java/com/salemale/domain/search/service/KeywordItemSearchServiceImpl.java +++ b/src/main/java/com/salemale/domain/search/service/KeywordItemSearchServiceImpl.java @@ -10,6 +10,7 @@ import com.salemale.domain.item.repository.ItemRepository; import com.salemale.domain.user.entity.User; import com.salemale.domain.user.entity.UserRegion; +import com.salemale.domain.user.repository.BlockListRepository; import com.salemale.domain.user.repository.UserRegionRepository; import com.salemale.domain.user.repository.UserRepository; import com.salemale.global.common.enums.ItemStatus; @@ -21,7 +22,6 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Service @@ -31,6 +31,8 @@ public class KeywordItemSearchServiceImpl implements KeywordItemSearchService { private final UserRepository userRepository; private final UserRegionRepository userRegionRepository; private final ItemRepository itemRepository; + private final BlockListRepository blockListRepository; + @Override @Transactional(readOnly = true) @@ -44,9 +46,11 @@ public Page search( Integer maxPrice, AuctionSortType sort, Pageable pageable) { + // q가 null이거나 비어있으면 키워드 검색 없이 필터만 적용 String keyword = (q != null && !q.trim().isBlank()) ? q.trim() : null; + // 비로그인 사용자: 전체 지역 표시로 전국 검색 if (userIdOpt.isEmpty()) { // 비로그인 사용자는 radius 파라미터를 무시하고 항상 전국 검색 @@ -71,14 +75,14 @@ public Page search( // 키워드가 있으면 키워드 검색 + 필터 Page page = itemRepository.searchItemsByKeywordWithFilters( effectiveStatus, keyword, cats, normalizeMin(minPrice), normalizeMax(maxPrice), - isPopular, threeDaysAgo, now, null, sorted + isPopular, threeDaysAgo, now, sorted ); return page.map(ItemConverter::toAuctionListItemDTO); } else { // 키워드가 없으면 필터만 적용 (키워드 조건 제외) Page page = itemRepository.searchItemsByFiltersOnly( effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), - isPopular, threeDaysAgo, now, null, sorted + isPopular, threeDaysAgo, now, sorted ); return page.map(ItemConverter::toAuctionListItemDTO); } @@ -92,6 +96,10 @@ effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), UserRegion primary = userRegionRepository.findByPrimaryUser(user) .orElseThrow(() -> new GeneralException(ErrorStatus.REGION_NOT_SET)); + // 내가 차단한 판매자 ID 목록 + List blockedSellerIds = + blockListRepository.findBlockedUserIds(userId); + Double effective = user.getRangeInKilometers(); if (radius != null && radius != User.RangeSetting.ALL) { effective = Math.max(radius.toKilometers(), 0.1); @@ -103,6 +111,7 @@ effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), double lat = primary.getRegion().getLatitude().doubleValue(); double lon = primary.getRegion().getLongitude().doubleValue(); + // COMPLETED 상태는 SUCCESS와 FAIL 둘 다 포함 if (isCompletedStatus(status)) { java.util.List cats = (categories == null || categories.isEmpty()) ? null : categories; @@ -114,7 +123,13 @@ effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), Page page = nationwide ? searchCompletedItems(keyword, cats, normalizeMin(minPrice), normalizeMax(maxPrice), sorted, userId) : searchCompletedItemsNearby(keyword, lat, lon, km, cats, normalizeMin(minPrice), normalizeMax(maxPrice), sort, pageable, userId); - return page.map(ItemConverter::toAuctionListItemDTO); + + + return page.map(item -> { + boolean blockedSeller = + blockedSellerIds.contains(item.getSeller().getId()); + return ItemConverter.toAuctionListItemDTO(item, blockedSeller); + }); } com.salemale.global.common.enums.ItemStatus effectiveStatus = mapAuctionStatusToItemStatus(status); @@ -136,23 +151,24 @@ effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), // 키워드가 있으면 키워드 검색 + 필터 page = itemRepository.searchItemsByKeywordWithFilters( effectiveStatus, keyword, cats, normalizeMin(minPrice), normalizeMax(maxPrice), - isPopular, threeDaysAgo, now, userId, sorted + isPopular, threeDaysAgo, now, sorted ); } else { // 키워드가 없으면 필터만 적용 (키워드 조건 제외) page = itemRepository.searchItemsByFiltersOnly( effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), - isPopular, threeDaysAgo, now, userId, sorted + isPopular, threeDaysAgo, now, sorted ); } + } else { // 반경 검색 if (keyword != null) { // 키워드가 있으면 키워드 + 반경 검색 - page = itemRepository.findNearbyItemsByKeyword(effectiveStatus.name(), keyword, lat, lon, km, userId, pageable); + page = itemRepository.findNearbyItemsByKeyword(effectiveStatus.name(), keyword, lat, lon, km, pageable); } else { // 키워드가 없으면 반경 검색만 (키워드 조건 제외) - page = itemRepository.findNearbyItems(effectiveStatus.name(), lat, lon, km, userId, pageable); + page = itemRepository.findNearbyItems(effectiveStatus.name(), lat, lon, km, pageable); } // 반경 검색의 경우 카테고리/가격 필터 및 POPULAR 조건은 메모리에서 보정 @@ -174,12 +190,22 @@ effectiveStatus, cats, normalizeMin(minPrice), normalizeMax(maxPrice), } // 페이지네이션 유지: 이미 DB 페이징이 되어 있으므로, 정렬만 보정하여 DTO 매핑 return new org.springframework.data.domain.PageImpl<>( - filtered.stream().map(ItemConverter::toAuctionListItemDTO).toList(), + filtered.stream() + .map(item -> { + boolean blockedSeller = + blockedSellerIds.contains(item.getSeller().getId()); + return ItemConverter.toAuctionListItemDTO(item, blockedSeller); + }) + .toList(), pageable, page.getTotalElements() ); } - return page.map(ItemConverter::toAuctionListItemDTO); + return page.map(item -> { + boolean blockedSeller = + blockedSellerIds.contains(item.getSeller().getId()); + return ItemConverter.toAuctionListItemDTO(item, blockedSeller); + }); } /** @@ -212,12 +238,12 @@ private Page searchCompletedItems(String keyword, java.util.List LocalDateTime now = LocalDateTime.now(); LocalDateTime threeDaysAgo = now.minusDays(3); Page successPage = keyword != null - ? itemRepository.searchItemsByKeywordWithFilters(ItemStatus.SUCCESS, keyword, categories, minPrice, maxPrice, false, threeDaysAgo, now, userId, pageable) - : itemRepository.searchItemsByFiltersOnly(ItemStatus.SUCCESS, categories, minPrice, maxPrice, false, threeDaysAgo, now, userId, pageable); + ? itemRepository.searchItemsByKeywordWithFilters(ItemStatus.SUCCESS, keyword, categories, minPrice, maxPrice, false, threeDaysAgo, now, pageable) + : itemRepository.searchItemsByFiltersOnly(ItemStatus.SUCCESS, categories, minPrice, maxPrice, false, threeDaysAgo, now, pageable); Page failPage = keyword != null - ? itemRepository.searchItemsByKeywordWithFilters(ItemStatus.FAIL, keyword, categories, minPrice, maxPrice, false, threeDaysAgo, now, userId, pageable) - : itemRepository.searchItemsByFiltersOnly(ItemStatus.FAIL, categories, minPrice, maxPrice, false, threeDaysAgo, now, userId, pageable); + ? itemRepository.searchItemsByKeywordWithFilters(ItemStatus.FAIL, keyword, categories, minPrice, maxPrice, false, threeDaysAgo, now, pageable) + : itemRepository.searchItemsByFiltersOnly(ItemStatus.FAIL, categories, minPrice, maxPrice, false, threeDaysAgo, now, pageable); // 두 결과를 합치기 java.util.List combined = new java.util.ArrayList<>(); @@ -245,12 +271,12 @@ private Page searchCompletedItems(String keyword, java.util.List private Page searchCompletedItemsNearby(String keyword, double lat, double lon, double km, java.util.List categories, Integer minPrice, Integer maxPrice, AuctionSortType sort, Pageable pageable, Long userId) { // SUCCESS와 FAIL 둘 다 조회 Page successPage = keyword != null - ? itemRepository.findNearbyItemsByKeyword(ItemStatus.SUCCESS.name(), keyword, lat, lon, km, userId, pageable) - : itemRepository.findNearbyItems(ItemStatus.SUCCESS.name(), lat, lon, km, userId, pageable); + ? itemRepository.findNearbyItemsByKeyword(ItemStatus.SUCCESS.name(), keyword, lat, lon, km, pageable) + : itemRepository.findNearbyItems(ItemStatus.SUCCESS.name(), lat, lon, km, pageable); Page failPage = keyword != null - ? itemRepository.findNearbyItemsByKeyword(ItemStatus.FAIL.name(), keyword, lat, lon, km, userId, pageable) - : itemRepository.findNearbyItems(ItemStatus.FAIL.name(), lat, lon, km, userId, pageable); + ? itemRepository.findNearbyItemsByKeyword(ItemStatus.FAIL.name(), keyword, lat, lon, km, pageable) + : itemRepository.findNearbyItems(ItemStatus.FAIL.name(), lat, lon, km, pageable); // 두 결과를 합치기 java.util.List combined = new java.util.ArrayList<>(); @@ -319,7 +345,7 @@ public Page searchCompletedItems(String q, Pageable pageable // 낙찰된 상품만 조회 (ItemStatus.SUCCESS), 날짜순 정렬 (최신순) ItemStatus status = ItemStatus.SUCCESS; - Page page = itemRepository.searchItemsByKeyword(status, keyword, null, pageable); + Page page = itemRepository.searchItemsByKeyword(status, keyword, pageable); return page.map(ItemConverter::toAuctionListItemDTO); } diff --git a/src/main/java/com/salemale/domain/search/service/NearbyItemSearchServiceImpl.java b/src/main/java/com/salemale/domain/search/service/NearbyItemSearchServiceImpl.java index 5fbcb46..9b5285f 100644 --- a/src/main/java/com/salemale/domain/search/service/NearbyItemSearchServiceImpl.java +++ b/src/main/java/com/salemale/domain/search/service/NearbyItemSearchServiceImpl.java @@ -8,6 +8,7 @@ import com.salemale.domain.item.repository.ItemRepository; import com.salemale.domain.user.entity.User; import com.salemale.domain.user.entity.UserRegion; +import com.salemale.domain.user.repository.BlockListRepository; import com.salemale.domain.user.repository.UserRegionRepository; import com.salemale.domain.user.repository.UserRepository; import com.salemale.global.common.enums.ItemStatus; @@ -17,6 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + + @Service @RequiredArgsConstructor public class NearbyItemSearchServiceImpl implements NearbyItemSearchService { @@ -24,6 +28,8 @@ public class NearbyItemSearchServiceImpl implements NearbyItemSearchService { private final UserRepository userRepository; private final UserRegionRepository userRegionRepository; private final ItemRepository itemRepository; + private final BlockListRepository blockListRepository; + @Override @Transactional(readOnly = true) @@ -36,8 +42,17 @@ public Page findNearbyItemsForUser(Long userId, Pageable pag double lat = primary.getRegion().getLatitude().doubleValue(); double lon = primary.getRegion().getLongitude().doubleValue(); - Page page = itemRepository.findNearbyItems(ItemStatus.BIDDING.name(), lat, lon, km, userId, pageable); - return page.map(ItemConverter::toAuctionListItemDTO); + // 내가 차단한 판매자 ID 목록 + List blockedSellerIds = + blockListRepository.findBlockedUserIds(userId); + + Page page = itemRepository.findNearbyItems(ItemStatus.BIDDING.name(), lat, lon, km, pageable); + + return page.map(item -> { + boolean blockedSeller = + blockedSellerIds.contains(item.getSeller().getId()); + return ItemConverter.toAuctionListItemDTO(item, blockedSeller); + }); } }