diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/projection/sales/PeakTimeAvgProjection.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/projection/sales/PeakTimeAvgProjection.java index 9bca8d01f..28f984cab 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/projection/sales/PeakTimeAvgProjection.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/projection/sales/PeakTimeAvgProjection.java @@ -3,5 +3,7 @@ /** SLS_13_01 (피크타임) */ public record PeakTimeAvgProjection( Integer timeSlot2H, // 2시간 슬롯 - Long orderCount // 주문 건수 + Long netAmount, // 실매출 평균 + Long orderCount, // 주문건수 평균 + Long operatingWeeks // 운영주차 ) {} diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/menu/IngredientUsageResponse.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/menu/IngredientUsageResponse.java index 62df0eb89..41cbaa890 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/menu/IngredientUsageResponse.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/menu/IngredientUsageResponse.java @@ -5,7 +5,9 @@ import java.util.List; /** MNU_04 (식자재 소진량) */ -public record IngredientUsageResponse(List items) +public record IngredientUsageResponse( + boolean hasIngredient, // 매장 식재료 등록 여부 + List items) implements DashboardAnalysisResponse, DetailAnalysisResponse { public record IngredientUsageItem( diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DashboardPeakTimeResponse.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DashboardPeakTimeResponse.java index 72a613c7b..1bfc9a10a 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DashboardPeakTimeResponse.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DashboardPeakTimeResponse.java @@ -6,11 +6,11 @@ /** SLS_13_01 (피크타임) */ public record DashboardPeakTimeResponse( int timeSlot2H, // 2시간 슬롯 - long orderCount, // 주문 건수 - long netAmount, // 실매출 - Integer todayPeak, - Integer comparisonPeak, - Integer diff, - ShiftDirection shiftDirection, - boolean beforeComparisonPeak) - implements DashboardAnalysisResponse {} + double orderCount, // 주문 건수 + double netAmount, // 실매출 + Integer todayPeak, // 오늘 peak 시간 + Integer comparisonPeak, // 비교 기간 peak 시간 + Integer diff, // |todayPeak - comparisonPeak| + ShiftDirection shiftDirection, // 비교 기간 대비 peak 이동 방향 + boolean beforeComparisonPeak // 현재 시간이 비교 기간 peak 이전인지 + ) implements DashboardAnalysisResponse {} diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DetailPeakTimeResponse.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DetailPeakTimeResponse.java index f94a1c0fd..54ae111ce 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DetailPeakTimeResponse.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/DetailPeakTimeResponse.java @@ -1,8 +1,16 @@ package com.checkmate.backend.domain.analysis.dto.response.sales; import com.checkmate.backend.domain.analysis.dto.response.DetailAnalysisResponse; +import com.checkmate.backend.domain.analysis.enums.ShiftDirection; import java.util.List; /** SLS_13_01 (피크타임) */ -public record DetailPeakTimeResponse(List items // 시간대별 주문건수 및 실매출 +public record DetailPeakTimeResponse( + List todayItems, + List week4Items, + Integer todayPeak, // 오늘 peak 시간 + Integer comparisonPeak, // 비교 기간 peak 시간 + Integer diff, // |todayPeak - comparisonPeak| + ShiftDirection shiftDirection, // 비교 기간 대비 peak 이동 방향 + boolean beforeComparisonPeak // 현재 시간이 비교 기간 peak 이전인지 ) implements DetailAnalysisResponse {} diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/PeakTimeItem.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/PeakTimeItem.java index 8ad034d26..a870423e1 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/PeakTimeItem.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/dto/response/sales/PeakTimeItem.java @@ -5,12 +5,14 @@ /** SLS_13_01 (피크타임) */ public record PeakTimeItem( int timeSlot2H, // 2시간 슬롯 - long orderCount, // 주문 건수 - long netAmount // 실매출 + Double orderCount, // 주문 건수 + Double netAmount // 실매출 ) { public static PeakTimeItem of(TodayPeakTimeProjection projection) { return new PeakTimeItem( - projection.timeSlot2H(), projection.orderCount(), projection.netAmount()); + projection.timeSlot2H(), + (double) projection.orderCount(), + (double) projection.netAmount()); } } diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/IngredientUsageProcessor.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/IngredientUsageProcessor.java index 09e0d66f8..fb9e1b6c2 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/IngredientUsageProcessor.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/IngredientUsageProcessor.java @@ -32,6 +32,9 @@ public boolean supports(AnalysisCardCode analysisCardCode) { @Override public AnalysisResponse process(MenuAnalysisContext context) { + boolean hasIngredient = + ingredientRepository.existsIngredientsByStoreId(context.getStoreId()); + // 식재료 사용량 갖고 온다. List ingredientUsageProjections = menuAnalysisRepository.findIngredientUsage( @@ -63,7 +66,8 @@ public AnalysisResponse process(MenuAnalysisContext context) { baseUnit)); } - IngredientUsageResponse response = new IngredientUsageResponse(ingredientUsageItems); + IngredientUsageResponse response = + new IngredientUsageResponse(hasIngredient, ingredientUsageItems); return new AnalysisResponse(context.getAnalysisCardCode(), response, response); } diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/PopularMenuCombinationProcessor.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/PopularMenuCombinationProcessor.java index 6bbaabe9c..56b41e990 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/PopularMenuCombinationProcessor.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/menu/PopularMenuCombinationProcessor.java @@ -144,17 +144,23 @@ public AnalysisResponse process(MenuAnalysisContext context) { pairedMenus)); } - DetailPopularMenuCombinationResponse.PopularMenuCombinationItem popularMenuCombinationItem = - items.get(0); - - String firstMenuName = popularMenuCombinationItem.baseMenuName(); - DetailPopularMenuCombinationResponse.PopularMenuCombinationItem.PairedMenuItem - pairedMenuItem = popularMenuCombinationItem.pairedMenus().get(0); - String secondMenuName = pairedMenuItem.menuName(); - DetailPopularMenuCombinationResponse response = new DetailPopularMenuCombinationResponse(items); + String firstMenuName = null; + String secondMenuName = null; + + if (!items.isEmpty()) { + + DetailPopularMenuCombinationResponse.PopularMenuCombinationItem firstItem = + items.get(0); + + if (!firstItem.pairedMenus().isEmpty()) { + firstMenuName = firstItem.baseMenuName(); + secondMenuName = firstItem.pairedMenus().get(0).menuName(); + } + } + DashboardPopularMenuCombinationResponse dashboardResponse = new DashboardPopularMenuCombinationResponse(firstMenuName, secondMenuName); diff --git a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/sales/PeakTimeProcessor.java b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/sales/PeakTimeProcessor.java index 6246e3202..764688339 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/sales/PeakTimeProcessor.java +++ b/backend/src/main/java/com/checkmate/backend/domain/analysis/processor/sales/PeakTimeProcessor.java @@ -47,10 +47,12 @@ public AnalysisResponse process(SalesAnalysisContext context) { List peakTimeAvgProjections = salesAnalysisRepository.findPeakTimeAvg( context.getStoreId(), - context.getStartDate(), - context.getEndDate(), + context.getComparisonStart(), + context.getComparisonEnd(), dayOfWeekValue); + // 운영 주차를 갖고 와야 함 + // 피크 계산 Integer todayPeak = findPeakHourToday(todayPeakTimeProjections); Integer comparisonPeak = findPeakHourAvg(peakTimeAvgProjections); @@ -62,24 +64,30 @@ public AnalysisResponse process(SalesAnalysisContext context) { // 현재 시점이 비교 피크 이전인지 boolean beforeComparisonPeak = isBeforeBaselinePeak(anchor, comparisonPeak); - // 없는 슬롯은 0을 채운다 - Map itemMap = + /* + * 현재 시점 + * */ + + // 없는 슬롯은 null로 채운다 + Map todayItemMap = todayPeakTimeProjections.stream() .map(PeakTimeItem::of) .collect(Collectors.toMap(PeakTimeItem::timeSlot2H, Function.identity())); - List items = new ArrayList<>(); + List todayItems = new ArrayList<>(); for (int slot = 0; slot <= 22; slot += 2) { - items.add(itemMap.getOrDefault(slot, new PeakTimeItem(slot, 0L, 0L))); + todayItems.add(todayItemMap.getOrDefault(slot, new PeakTimeItem(slot, null, null))); } int currentTimeSlot = TimeUtil.get2HourSlot(anchor); - PeakTimeItem currentItem = itemMap.get(currentTimeSlot); + PeakTimeItem currentItem = todayItemMap.get(currentTimeSlot); - long orderCount = Optional.ofNullable(currentItem).map(PeakTimeItem::orderCount).orElse(0L); + double orderCount = + Optional.ofNullable(currentItem).map(PeakTimeItem::orderCount).orElse(0.0); - long netAmount = Optional.ofNullable(currentItem).map(PeakTimeItem::netAmount).orElse(0L); + double netAmount = + Optional.ofNullable(currentItem).map(PeakTimeItem::netAmount).orElse(0.0); DashboardPeakTimeResponse dashboard = new DashboardPeakTimeResponse( @@ -92,7 +100,36 @@ public AnalysisResponse process(SalesAnalysisContext context) { direction, beforeComparisonPeak); - DetailPeakTimeResponse detail = new DetailPeakTimeResponse(items); + /* + * 비교기간 + * */ + + // 없는 슬롯은 null로 채운다 + Map week4ItemMap = + peakTimeAvgProjections.stream() + .map( + p -> + new PeakTimeItem( + p.timeSlot2H(), + (double) p.orderCount() / p.operatingWeeks(), + (double) p.netAmount() / p.operatingWeeks())) + .collect(Collectors.toMap(PeakTimeItem::timeSlot2H, Function.identity())); + + List week4Items = new ArrayList<>(); + + for (int slot = 0; slot <= 22; slot += 2) { + week4Items.add(week4ItemMap.getOrDefault(slot, new PeakTimeItem(slot, null, null))); + } + + DetailPeakTimeResponse detail = + new DetailPeakTimeResponse( + todayItems, + week4Items, + todayPeak, + comparisonPeak, + diff, + direction, + beforeComparisonPeak); return new AnalysisResponse(context.getAnalysisCardCode(), dashboard, detail); } @@ -120,21 +157,21 @@ private Integer calculateDiff(Integer todayPeak, Integer comparisonPeak) { return Math.abs(todayPeak - comparisonPeak); } - private ShiftDirection resolveDirection(Integer today, Integer baseline) { + private ShiftDirection resolveDirection(Integer today, Integer comparisonPeak) { - if (today == null || baseline == null) return ShiftDirection.UNKNOWN; + if (today == null || comparisonPeak == null) return ShiftDirection.UNKNOWN; - if (today.equals(baseline)) return ShiftDirection.SAME; + if (today.equals(comparisonPeak)) return ShiftDirection.SAME; - return today < baseline ? ShiftDirection.EARLY : ShiftDirection.LATE; + return today < comparisonPeak ? ShiftDirection.EARLY : ShiftDirection.LATE; } - private boolean isBeforeBaselinePeak(LocalDateTime anchor, Integer baselinePeak) { + private boolean isBeforeBaselinePeak(LocalDateTime anchor, Integer comparisonPeak) { - if (baselinePeak == null) return false; + if (comparisonPeak == null) return false; int currentHour = anchor.getHour(); - return currentHour < baselinePeak; + return currentHour < comparisonPeak; } } diff --git a/backend/src/main/java/com/checkmate/backend/domain/menu/repository/IngredientRepository.java b/backend/src/main/java/com/checkmate/backend/domain/menu/repository/IngredientRepository.java index 19fe70a37..e3437909b 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/menu/repository/IngredientRepository.java +++ b/backend/src/main/java/com/checkmate/backend/domain/menu/repository/IngredientRepository.java @@ -46,4 +46,7 @@ List findNameByStoreIdAndKeyword( @Query("select i from Ingredient i where i.id in :ingredientIds") List findAllByIds(@Param("ingredientIds") List ingredientIds); + + @Query("select count(i)>0 from Ingredient i" + " where i.store.id=:storeId") + boolean existsIngredientsByStoreId(@Param("storeId") Long storeId); } diff --git a/backend/src/main/java/com/checkmate/backend/domain/order/enums/OrderChannel.java b/backend/src/main/java/com/checkmate/backend/domain/order/enums/OrderChannel.java index af53ae981..a1e4f4c98 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/order/enums/OrderChannel.java +++ b/backend/src/main/java/com/checkmate/backend/domain/order/enums/OrderChannel.java @@ -7,7 +7,8 @@ public enum OrderChannel { POS("POS", "POS"), KIOSK("KIOSK", "키오스크"), - DELIVERY_APP("DELIVERY_APP", "배달앱"); + DELIVERY_APP("DELIVERY_APP", "배달앱"), + ETC("ETC", "기타"); private final String value; // DB 저장 값 private final String description; // UI/표시용 diff --git a/backend/src/main/java/com/checkmate/backend/domain/order/repository/SalesAnalysisRepository.java b/backend/src/main/java/com/checkmate/backend/domain/order/repository/SalesAnalysisRepository.java index cc2ada0b7..9e68a98e7 100644 --- a/backend/src/main/java/com/checkmate/backend/domain/order/repository/SalesAnalysisRepository.java +++ b/backend/src/main/java/com/checkmate/backend/domain/order/repository/SalesAnalysisRepository.java @@ -160,7 +160,7 @@ List findTodayPeakTime( // 비교기간 계산 쿼리 @Query( - "select new com.checkmate.backend.domain.analysis.dto.projection.sales.PeakTimeAvgProjection(o.timeSlot2H, count(*)) " + "select new com.checkmate.backend.domain.analysis.dto.projection.sales.PeakTimeAvgProjection(o.timeSlot2H, sum(o.netAmount), count(o), count(distinct (function('date_trunc','week',o.orderDate)))) " + " from Order o" + " where o.store.id=:storeId" + " and o.orderDate >= :startDate and o.orderDate < :endDate"