From 0839f0726a1f77f7268380f2d3e94d98c44f37f3 Mon Sep 17 00:00:00 2001 From: BAEK0111 Date: Sat, 21 Feb 2026 17:18:34 +0900 Subject: [PATCH] =?UTF-8?q?refactor/#249=20-=20=EC=9E=84=EC=8B=9C=EB=A1=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=A7=80=EB=8F=84=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=82=B4=EA=B0=80=20=EB=B0=9B=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EC=A0=9C=ED=9C=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20api=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/map/service/MapServiceImpl.java | 84 ++++++++++++------- .../repository/PaperContentRepository.java | 40 +++++++++ .../repository/PaperRepository.java | 14 ++++ 3 files changed, 109 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java index db6bacc8..73143a45 100644 --- a/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java +++ b/src/main/java/com/assu/server/domain/map/service/MapServiceImpl.java @@ -183,30 +183,58 @@ public List getStoresV2(MapRequestDTO.View final List stores = storeRepository.findAllWithinViewport(wkt); final List userPapers = userPaperRepository.findActivePartnershipsByStudentId(memberId, java.time.LocalDate.now()); + // 모든 매장 ID 수집 + List storeIds = stores.stream().map(Store::getId).toList(); + + // 빈 화면(매장 없음) 방어 코드 + if (storeIds.isEmpty()) { + return java.util.Collections.emptyList(); + } + + // 사용자가 가진 Paper ID 수집 (쿼리에 파라미터로 넘기기 위해 위치를 위로 올림!) + List userPaperIds = userPapers.stream() + .map(up -> up.getPaper().getId()) + .toList(); + + // 일괄 조회 1: 모든 매장의 PaperContent (내 혜택 기준 매장당 최대 2개) + List allContents; + if (userPaperIds.isEmpty()) { + // 방어 코드: 사용자가 가진 혜택이 없다면 IN () 에러 방지를 위해 쿼리 실행 없이 빈 리스트 할당 + allContents = java.util.Collections.emptyList(); + } else { + allContents = paperContentRepository.findLatestValidByStoreIdInNativeMax2( + storeIds, + ActivationStatus.ACTIVE.name(), + OptionType.SERVICE.name(), + OptionType.DISCOUNT.name(), + CriterionType.PRICE.name(), + CriterionType.HEADCOUNT.name(), + userPaperIds // ⭐️ 새로 추가된 파라미터 전달! + ); + } + + // 일괄 조회 2: 모든 매장의 Paper와 Admin 정보 (Fetch Join) + List allPapers = paperRepository.findLatestPapersByStoreIds(storeIds); + + // 매장별 Paper 매핑 (매장당 최신 1개) + java.util.Map paperByStore = allPapers.stream() + .collect(java.util.stream.Collectors.toMap( + p -> p.getStore().getId(), + p -> p, + (existing, replacement) -> existing // 이미 키가 있으면 기존 값(최신) 유지 + )); + + // 매장별로 PaperContent 그룹화 + java.util.Map> contentsByStore = allContents.stream() + .collect(java.util.stream.Collectors.groupingBy( + pc -> pc.getPaper().getStore().getId() + )); + return stores.stream().map(s -> { final boolean hasPartner = (s.getPartner() != null); - // 사용자의 제휴권으로 이 매장에서 받을 수 있는 혜택 수집 (최대 2개) - List benefits = new java.util.ArrayList<>(); - for (UserPaper userPaper : userPapers) { - if (benefits.size() >= 2) break; - - Paper paper = userPaper.getPaper(); - if (paper.getStore() != null && paper.getStore().getId().equals(s.getId())) { - PaperContent content = paperContentRepository.findLatestValidByStoreIdNative( - s.getId(), - ActivationStatus.ACTIVE.name(), - OptionType.SERVICE.name(), - OptionType.DISCOUNT.name(), - CriterionType.PRICE.name(), - CriterionType.HEADCOUNT.name() - ).orElse(null); - - if (content != null) { - benefits.add(content); - } - } - } + // ⭐️ DB에서 이미 '내 혜택 중 상위 2개'만 가져왔으므로 filter와 limit이 필요 없음! 꺼내기만 하면 끝. + List benefits = contentsByStore.getOrDefault(s.getId(), java.util.Collections.emptyList()); // 혜택 텍스트 생성 String partner1 = null; @@ -228,15 +256,13 @@ public List getStoresV2(MapRequestDTO.View benefit2 = generateBenefitText(content2); } - // admin 정보 - final Long adminId = paperRepository.findTopPaperByStoreId(s.getId()) - .map(p -> p.getAdmin() != null ? p.getAdmin().getId() : null) - .orElse(null); - + Paper paper = paperByStore.get(s.getId()); + Long adminId = null; String adminName = null; - if (adminId != null) { - final Admin admin = adminRepository.findById(adminId).orElse(null); - adminName = (admin != null ? admin.getName() : null); + if (paper != null && paper.getAdmin() != null) { + adminId = paper.getAdmin().getId(); + String name = paper.getAdmin().getName(); + adminName = (name != null) ? name : ""; // Null이면 빈 문자열로 덮어쓰기! } // S3 presigned URL diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java index 18ddf5f9..0d55171a 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperContentRepository.java @@ -66,6 +66,46 @@ Optional findLatestValidByStoreIdNative( @Param("headcount") String headcount // CriterionType.HEADCOUNT.name() ); + @Query(value = """ +WITH ranked_content AS ( + SELECT pc.*, + ROW_NUMBER() OVER ( + PARTITION BY p.store_id + ORDER BY + CASE pc.option_type + WHEN :service THEN 0 ELSE 1 END, + CASE pc.criterion_type + WHEN :price THEN 0 + WHEN :headcount THEN 1 + ELSE 2 END, + pc.updated_at DESC, + pc.id DESC + ) AS rn + FROM paper_content pc + JOIN paper p ON p.id = pc.paper_id + WHERE p.store_id IN :storeIds + AND p.id IN :userPaperIds -- 대장님이 추가하신 핵심 조건! + AND p.is_activated = :active + AND CURRENT_DATE BETWEEN p.partnership_period_start AND p.partnership_period_end + AND ( + (pc.option_type = :service AND + ((pc.criterion_type = :price AND pc.cost IS NOT NULL) + OR (pc.criterion_type = :headcount AND pc.cost IS NOT NULL AND pc.people IS NOT NULL))) + OR (pc.option_type = :discount AND pc.discount IS NOT NULL) + ) +) +SELECT * FROM ranked_content WHERE rn <= 2 +""", nativeQuery = true) + List findLatestValidByStoreIdInNativeMax2( + @Param("storeIds") List storeIds, + @Param("active") String active, + @Param("service") String service, + @Param("discount") String discount, + @Param("price") String price, + @Param("headcount") String headcount, + @Param("userPaperIds") List userPaperIds // 수정된 부분: 이름과 타입 변경 + ); + Optional findTopByPaperIdOrderByIdDesc(Long paperId); } diff --git a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java index a368fe45..670d951d 100644 --- a/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java +++ b/src/main/java/com/assu/server/domain/partnership/repository/PaperRepository.java @@ -61,4 +61,18 @@ List findActivePapersByAdminIds(@Param("adminIds") List adminIds, @Param("status") ActivationStatus status); List findByStoreIdAndAdminIdAndIsActivated(Long storeId, Long adminId, ActivationStatus isActivated); + + // PaperRepository.java에 추가 + @Query(""" + SELECT p + FROM Paper p + LEFT JOIN FETCH p.admin a + WHERE p.store.id IN :storeIds + AND p.isActivated = com.assu.server.domain.common.enums.ActivationStatus.ACTIVE + AND p.partnershipPeriodStart <= CURRENT_DATE + AND p.partnershipPeriodEnd >= CURRENT_DATE + ORDER BY p.id DESC +""") + List findLatestPapersByStoreIds(@Param("storeIds") List storeIds); + }