diff --git a/src/main/java/com/catchtable/store/entity/Store.java b/src/main/java/com/catchtable/store/entity/Store.java index a8230b8..8bf6e0c 100644 --- a/src/main/java/com/catchtable/store/entity/Store.java +++ b/src/main/java/com/catchtable/store/entity/Store.java @@ -14,9 +14,10 @@ @Table( name = "stores", indexes = { - @Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"), - @Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"), - @Index(name = "idx_store_deleted_district", columnList = "is_deleted, district") + @Index(name = "idx_store_lat_lng", columnList = "latitude, longitude"), + @Index(name = "idx_store_deleted_category", columnList = "is_deleted, category"), + @Index(name = "idx_store_deleted_district", columnList = "is_deleted, district"), + @Index(name = "idx_store_popularity", columnList = "is_deleted, average_star DESC, review_count DESC, bookmark_count DESC, id ASC") } ) @Getter diff --git a/src/main/java/com/catchtable/store/repository/StoreRepository.java b/src/main/java/com/catchtable/store/repository/StoreRepository.java index 2f0fa6c..caf86be 100644 --- a/src/main/java/com/catchtable/store/repository/StoreRepository.java +++ b/src/main/java/com/catchtable/store/repository/StoreRepository.java @@ -16,15 +16,10 @@ public interface StoreRepository extends JpaRepository, JpaSpecific List findAllByIsDeletedFalse(); - /** - * 인기 매장 정렬 — averageStar DESC → reviewCount DESC → bookmarkCount DESC → id ASC - * - averageStar는 NULL일 수 있어 COALESCE로 0 처리 - * - 모든 통계 값이 동률일 때 ID 오름차순(=먼저 등록된 순)으로 결정성 보장 - */ @Query(value = """ SELECT s FROM Store s WHERE s.isDeleted = false - ORDER BY COALESCE(s.averageStar, 0) DESC, + ORDER BY s.averageStar DESC, s.reviewCount DESC, s.bookmarkCount DESC, s.id ASC @@ -80,9 +75,9 @@ AND ST_DistanceSphere( ST_MakePoint(s.longitude, s.latitude), ST_MakePoint(:lon, :lat) ) <= :radiusMeters - ORDER BY COALESCE(s.average_star, 0) DESC, - COALESCE(s.review_count, 0) DESC, - COALESCE(s.bookmark_count, 0) DESC, + ORDER BY s.average_star DESC, + s.review_count DESC, + s.bookmark_count DESC, s.id ASC """, nativeQuery = true) @@ -121,12 +116,8 @@ List findNearbyWithGist(@Param("lat") double latitude, @Query(value = """ SELECT * FROM stores s WHERE s.is_deleted = false - AND s.latitude BETWEEN :minLat AND :maxLat - AND s.longitude BETWEEN :minLng AND :maxLng - ORDER BY ST_DistanceSphere( - ST_MakePoint(s.longitude, s.latitude), - ST_MakePoint(:centerLng, :centerLat) - ) ASC, + AND s.location && ST_MakeEnvelope(:minLng, :minLat, :maxLng, :maxLat, 4326)::geography + ORDER BY s.location <-> ST_SetSRID(ST_MakePoint(:centerLng, :centerLat), 4326)::geography ASC, s.id ASC """, nativeQuery = true) diff --git a/src/main/java/com/catchtable/store/service/StoreService.java b/src/main/java/com/catchtable/store/service/StoreService.java index 8f13f98..f07fdd1 100644 --- a/src/main/java/com/catchtable/store/service/StoreService.java +++ b/src/main/java/com/catchtable/store/service/StoreService.java @@ -45,6 +45,7 @@ public class StoreService { private final StoreRemainRepository storeRemainRepository; // 매장 등록 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreCreateResponse createStore(Long userId, StoreCreateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_CREATE); @@ -57,6 +58,7 @@ public StoreCreateResponse createStore(Long userId, StoreCreateRequest request) * 매장 목록 통합 조회 (이름·카테고리·지역 옵셔널 필터 + DB 페이지네이션 + 인기 정렬) * Specification 사용으로 PostgreSQL+enum 조합에서 :param IS NULL 회피. */ + @Cacheable(value = "storeList", key = "T(String).valueOf(#category) + ':' + T(String).valueOf(#district) + ':' + #page + ':' + #size", condition = "#name == null") @Transactional(readOnly = true) public List getStores(String name, Category category, District district, int page, int size) { int limitedSize = Math.min(size, 100); @@ -79,10 +81,9 @@ public List getPopularStores(int limit) { .toList(); } - @CacheEvict(value = "popularStores", allEntries = true) + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void evictPopularStoresCache() { - // 매장 등록/수정/리뷰 생성 등 인기도 변동 시 호출 } @Tool(description = "사용자 주변의 인기 매장을 조회합니다. '내 주변 맛집', '근처 인기 매장', '주변 맛집 추천' 등의 요청에 사용하세요. 위치 정보가 없으면 사용할 수 없습니다.") @@ -196,6 +197,7 @@ public StoreDetailResponse getStore(Long storeId) { } // 매장 정보 수정 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreUpdateResponse updateStore(Long userId, Long storeId, StoreUpdateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_UPDATE); @@ -210,6 +212,7 @@ public StoreUpdateResponse updateStore(Long userId, Long storeId, StoreUpdateReq } // 매장 상태 변경 + @CacheEvict(value = "storeList", allEntries = true) @Transactional public StoreStatusUpdateResponse updateStoreStatus(Long userId, Long storeId, StoreStatusUpdateRequest request) { userRepository.getAdminOrThrow(userId, ErrorCode.ADMIN_ONLY_STORE_STATUS); @@ -223,6 +226,7 @@ public StoreStatusUpdateResponse updateStoreStatus(Long userId, Long storeId, St * 자체 리뷰 생성 시 호출 — 외부 시드된 별점/리뷰수를 base로 평균에 합산. * 리스너에서 비동기로 호출되므로 리뷰 작성 자체엔 영향을 주지 않는다. */ + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewCreated(Long storeId, int newStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId) @@ -230,6 +234,7 @@ public void applyReviewCreated(Long storeId, int newStar) { store.applyReviewCreated(newStar); } + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewDeleted(Long storeId, int deletedStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId) @@ -237,6 +242,7 @@ public void applyReviewDeleted(Long storeId, int deletedStar) { store.applyReviewDeleted(deletedStar); } + @CacheEvict(value = {"popularStores", "storeList"}, allEntries = true) @Transactional public void applyReviewUpdated(Long storeId, int oldStar, int newStar) { Store store = storeRepository.findByIdAndIsDeletedFalse(storeId)