1313import org .springframework .stereotype .Component ;
1414import org .springframework .transaction .annotation .Transactional ;
1515
16+ import com .loopers .application .ranking .MonthlyRankingService ;
17+ import com .loopers .application .ranking .WeeklyRankingService ;
1618import com .loopers .cache .CacheStrategy ;
1719import com .loopers .cache .RankingRedisService ;
1820import com .loopers .cache .dto .CachePayloads .RankingItem ;
2123import com .loopers .domain .like .LikeService ;
2224import com .loopers .domain .product .*;
2325import com .loopers .domain .product .dto .ProductSearchFilter ;
26+ import com .loopers .domain .ranking .MonthlyRankEntity ;
27+ import com .loopers .domain .ranking .RankingPeriod ;
28+ import com .loopers .domain .ranking .WeeklyRankEntity ;
2429import com .loopers .domain .tracking .UserBehaviorTracker ;
2530import com .loopers .domain .user .UserService ;
2631
@@ -46,6 +51,8 @@ public class ProductFacade {
4651 private final BrandService brandService ;
4752 private final UserBehaviorTracker behaviorTracker ;
4853 private final RankingRedisService rankingRedisService ;
54+ private final WeeklyRankingService weeklyRankingService ;
55+ private final MonthlyRankingService monthlyRankingService ;
4956
5057 /**
5158 * 도메인 서비스에서 MV 엔티티를 조회하고, Facade에서 DTO로 변환합니다.
@@ -70,7 +77,7 @@ public Page<ProductInfo> getProducts(ProductSearchFilter productSearchFilter) {
7077 * 도메인 서비스에서 엔티티를 조회하고, Facade에서 DTO로 변환합니다.
7178 *
7279 * @param productId 상품 ID
73- * @param username 사용자 ID (nullable)
80+ * @param username 사용자 ID (nullable)
7481 * @return 상품 상세 정보
7582 */
7683 @ Transactional (readOnly = true )
@@ -129,25 +136,25 @@ public ProductDetailInfo getProductDetail(Long productId, String username) {
129136 @ Transactional (readOnly = true )
130137 public Page <ProductInfo > getRankingProducts (Pageable pageable , LocalDate date ) {
131138 LocalDate targetDate = date != null ? date : LocalDate .now ();
132-
139+
133140 // 1. 랭킹 조회
134141 List <RankingItem > rankings = rankingRedisService .getRanking (
135142 targetDate ,
136- pageable .getPageNumber () + 1 ,
143+ pageable .getPageNumber () + 1 ,
137144 pageable .getPageSize ()
138145 );
139146
140147 // 2. 콜드 스타트 Fallback: 오늘 랭킹이 비어있으면 어제 랭킹 조회
141148 if (rankings .isEmpty () && date == null ) {
142149 LocalDate yesterday = targetDate .minusDays (1 );
143150 log .info ("콜드 스타트 Fallback: 오늘({}) 랭킹 없음, 어제({}) 랭킹 조회" , targetDate , yesterday );
144-
151+
145152 rankings = rankingRedisService .getRanking (
146153 yesterday ,
147154 pageable .getPageNumber () + 1 ,
148155 pageable .getPageSize ()
149156 );
150-
157+
151158 if (!rankings .isEmpty ()) {
152159 targetDate = yesterday ; // totalCount 계산을 위해 날짜 변경
153160 }
@@ -181,6 +188,119 @@ public Page<ProductInfo> getRankingProducts(Pageable pageable, LocalDate date) {
181188 return new PageImpl <>(sortedProducts , pageable , totalCount );
182189 }
183190
191+ /**
192+ * 기간별 랭킹 상품 목록 조회
193+ *
194+ * @param period 랭킹 기간 (DAILY, WEEKLY, MONTHLY)
195+ * @param pageable 페이징 정보
196+ * @param date 조회 날짜 (DAILY용, null이면 오늘)
197+ * @param yearWeek 조회 주차 (WEEKLY용, 예: "2024-W52")
198+ * @param yearMonth 조회 월 (MONTHLY용, 예: "2024-12")
199+ * @return 랭킹 상품 목록
200+ */
201+ @ Transactional (readOnly = true )
202+ public Page <ProductInfo > getRankingProductsByPeriod (
203+ RankingPeriod period ,
204+ Pageable pageable ,
205+ LocalDate date ,
206+ String yearWeek ,
207+ String yearMonth ) {
208+
209+ return switch (period ) {
210+ case DAILY -> getRankingProducts (pageable , date );
211+ case WEEKLY -> getWeeklyRankingProducts (pageable , yearWeek );
212+ case MONTHLY -> getMonthlyRankingProducts (pageable , yearMonth );
213+ };
214+ }
215+
216+ /**
217+ * 주간 랭킹 상품 목록 조회
218+ *
219+ * @param pageable 페이징 정보
220+ * @param yearWeek 조회 주차 (예: "2024-W52")
221+ * @return 주간 랭킹 상품 목록
222+ */
223+ @ Transactional (readOnly = true )
224+ public Page <ProductInfo > getWeeklyRankingProducts (Pageable pageable , String yearWeek ) {
225+ if (yearWeek == null || yearWeek .trim ().isEmpty ()) {
226+ log .warn ("주간 랭킹 조회 시 yearWeek 파라미터가 필요합니다" );
227+ return Page .empty (pageable );
228+ }
229+
230+ // 1. 주간 랭킹 조회
231+ Page <WeeklyRankEntity > weeklyRankings = weeklyRankingService .getWeeklyRanking (yearWeek , pageable );
232+
233+ if (weeklyRankings .isEmpty ()) {
234+ log .debug ("주간 랭킹 데이터 없음: yearWeek={}" , yearWeek );
235+ return Page .empty (pageable );
236+ }
237+
238+ // 2. 상품 ID 목록 추출
239+ List <Long > productIds = weeklyRankings .getContent ().stream ()
240+ .map (WeeklyRankEntity ::getProductId )
241+ .collect (Collectors .toList ());
242+
243+ // 3. 상품 정보 조회 (MV 사용)
244+ List <ProductMaterializedViewEntity > products = mvService .getByIds (productIds );
245+
246+ // 4. 랭킹 순서대로 정렬
247+ List <ProductInfo > sortedProducts = productIds .stream ()
248+ .map (productId -> products .stream ()
249+ .filter (p -> p .getProductId ().equals (productId ))
250+ .findFirst ()
251+ .map (ProductInfo ::from )
252+ .orElse (null ))
253+ .filter (Objects ::nonNull )
254+ .collect (Collectors .toList ());
255+
256+ // 5. Page 객체 생성
257+ return new PageImpl <>(sortedProducts , pageable , weeklyRankings .getTotalElements ());
258+ }
259+
260+ /**
261+ * 월간 랭킹 상품 목록 조회
262+ *
263+ * @param pageable 페이징 정보
264+ * @param yearMonth 조회 월 (예: "2024-12")
265+ * @return 월간 랭킹 상품 목록
266+ */
267+ @ Transactional (readOnly = true )
268+ public Page <ProductInfo > getMonthlyRankingProducts (Pageable pageable , String yearMonth ) {
269+ if (yearMonth == null || yearMonth .trim ().isEmpty ()) {
270+ log .warn ("월간 랭킹 조회 시 yearMonth 파라미터가 필요합니다" );
271+ return Page .empty (pageable );
272+ }
273+
274+ // 1. 월간 랭킹 조회
275+ Page <MonthlyRankEntity > monthlyRankings = monthlyRankingService .getMonthlyRanking (yearMonth , pageable );
276+
277+ if (monthlyRankings .isEmpty ()) {
278+ log .debug ("월간 랭킹 데이터 없음: yearMonth={}" , yearMonth );
279+ return Page .empty (pageable );
280+ }
281+
282+ // 2. 상품 ID 목록 추출
283+ List <Long > productIds = monthlyRankings .getContent ().stream ()
284+ .map (MonthlyRankEntity ::getProductId )
285+ .collect (Collectors .toList ());
286+
287+ // 3. 상품 정보 조회 (MV 사용)
288+ List <ProductMaterializedViewEntity > products = mvService .getByIds (productIds );
289+
290+ // 4. 랭킹 순서대로 정렬
291+ List <ProductInfo > sortedProducts = productIds .stream ()
292+ .map (productId -> products .stream ()
293+ .filter (p -> p .getProductId ().equals (productId ))
294+ .findFirst ()
295+ .map (ProductInfo ::from )
296+ .orElse (null ))
297+ .filter (Objects ::nonNull )
298+ .collect (Collectors .toList ());
299+
300+ // 5. Page 객체 생성
301+ return new PageImpl <>(sortedProducts , pageable , monthlyRankings .getTotalElements ());
302+ }
303+
184304 /**
185305 * 상품을 삭제합니다.
186306 * <p>
0 commit comments