Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nowait-app-admin-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation project(':nowait-domain:domain-admin-rdb')
implementation project(':nowait-domain:domain-core-rdb')
implementation project(':nowait-domain:domain-redis')
implementation project(':nowait-event')

// Spring Boot Starter
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -24,6 +25,7 @@
import com.nowait.domaincorerdb.user.entity.User;
import com.nowait.domaincorerdb.user.exception.UserNotFoundException;
import com.nowait.domaincorerdb.user.repository.UserRepository;
import com.nowait.nowaitevent.order.event.CookingCompleteEvent;

import lombok.RequiredArgsConstructor;

Expand All @@ -34,6 +36,7 @@ public class OrderService {
private final StatisticCustomRepository statisticCustomRepository;
private final UserRepository userRepository;
private final StoreRepository storeRepository;
private final ApplicationEventPublisher publisher;

@Transactional(readOnly = true)
public List<OrderResponseDto> findAllOrders(Long storeId, MemberDetails memberDetails) {
Expand All @@ -57,6 +60,23 @@ public OrderStatusUpdateResponseDto updateOrderStatus(Long orderId, OrderStatus
throw new OrderUpdateUnauthorizedException();
}
userOrder.updateStatus(newStatus);

if (OrderStatus.COOKED.equals(newStatus)) {
List<CookingCompleteEvent.Item> items = userOrder.getOrderItems().stream()
.map(item -> new CookingCompleteEvent.Item(
item.getMenu().getId(),
item.getQuantity()
))
.toList();

publisher.publishEvent(
new CookingCompleteEvent(
userOrder.getStore().getStoreId(),
items
)
);
}

return OrderStatusUpdateResponseDto.fromEntity(userOrder);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import org.springframework.web.bind.annotation.RestController;

import com.nowait.applicationadmin.order.service.OrderService;
import com.nowait.applicationadmin.statistic.dto.PopularMenuDto;
import com.nowait.applicationadmin.statistic.dto.StoreRankingDto;
import com.nowait.applicationadmin.statistic.service.PopularMenuRedisService;
import com.nowait.applicationadmin.statistic.service.RankingService;
import com.nowait.common.api.ApiUtils;
import com.nowait.domainadminrdb.statistic.dto.OrderSalesSumDetail;
Expand All @@ -29,6 +31,7 @@ public class StatisticsController {

private final OrderService orderService;
private final RankingService rankingService;
private final PopularMenuRedisService popularMenuRedisService;

@GetMapping("/sales")
@Operation(summary = "오늘의 매출 조회", description = "오늘의 매출을 조회합니다.")
Expand Down Expand Up @@ -58,4 +61,20 @@ public ResponseEntity<?> getTopSalesStores(@AuthenticationPrincipal MemberDetail
)
);
}

@GetMapping("/popular-menu")
@Operation(summary = "인기 메뉴 조회", description = "인기 메뉴를 조회합니다.")
@ApiResponse(responseCode = "200", description = "인기 메뉴 조회 성공")
public ResponseEntity<?> getPopularMenu(@AuthenticationPrincipal MemberDetails memberDetails) {

List<PopularMenuDto> popularMenu = popularMenuRedisService.getTodayTop5(memberDetails);

return ResponseEntity
.status(HttpStatus.OK)
.body(
ApiUtils.success(
popularMenu
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nowait.applicationadmin.statistic.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class PopularMenuDto {
private Long menuId;
private String menuName;
private Long soldCount;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

@Getter
public class StoreRankingDto {
private final Long storeId;
private final Long storeId;
private final String storeName;
private final Long departmentId;
private final Long departmentId;
private final String departmentName;
private final Integer totalSales;
private final Long currentRank;
private final Long currentRank;
private final Integer delta;
private final String profileUrl;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nowait.applicationadmin.statistic.service;

import java.util.List;

import com.nowait.applicationadmin.statistic.dto.PopularMenuDto;
import com.nowait.domaincorerdb.user.entity.MemberDetails;

public interface PopularMenuRedisService {
List<PopularMenuDto> getTodayTop5(MemberDetails memberDetails);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.nowait.applicationadmin.statistic.service.impl;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import com.nowait.applicationadmin.statistic.dto.PopularMenuDto;
import com.nowait.applicationadmin.statistic.service.PopularMenuRedisService;
import com.nowait.common.enums.Role;
import com.nowait.domainadminrdb.statistic.exception.StatisticViewUnauthorizedException;
import com.nowait.domaincorerdb.menu.entity.Menu;
import com.nowait.domaincorerdb.menu.repository.MenuRepository;
import com.nowait.domaincorerdb.user.entity.MemberDetails;
import com.nowait.domaincorerdb.user.entity.User;
import com.nowait.domaincorerdb.user.exception.UserNotFoundException;
import com.nowait.domaincorerdb.user.repository.UserRepository;
import com.nowait.domaincoreredis.rank.service.MenuCounterService;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class PopularMenuRedisServiceImpl implements PopularMenuRedisService {

private final MenuCounterService menuCounterService;
private final MenuRepository menuRepository;
private final UserRepository userRepository;

@Override
public List<PopularMenuDto> getTodayTop5(MemberDetails memberDetails) {

User user = userRepository.findById(memberDetails.getId()).orElseThrow(UserNotFoundException::new);
Long storeId = user.getStoreId();

if (!Role.SUPER_ADMIN.equals(user.getRole()) && !user.getStoreId().equals(storeId)) {
throw new StatisticViewUnauthorizedException();
}

Set<ZSetOperations.TypedTuple<String>> tuples = menuCounterService.getTopMenus(storeId, 5);

List<Long> menuIds = tuples.stream()
.map(tuple -> Long.parseLong(tuple.getValue()))
.toList();

Map<Long, String> menuIdToNameMap = menuRepository.findAllById(menuIds)
.stream()
.collect(Collectors.toMap(
Menu::getId,
Menu::getName
));


return tuples.stream()
.map(tuple -> {
Long menuId = Long.parseLong(tuple.getValue());
String menuName = menuIdToNameMap.getOrDefault(menuId, "Unknown Menu");

return new PopularMenuDto(menuId, menuName, tuple.getScore().longValue());
})
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum ErrorMessage {

// Statistics
STATISTIC_VIEW_UNAUTHORIZED("통계 보기 권한이 없습니다.(슈퍼계정 or 주점 관리자만 가능)", "statistics001"),
MENU_COUNTER_UPDATE("메뉴 카운터 업데이트 실패", "statistics002"),

// image
IMAGE_FILE_EMPTY("이미지 파일을 업로드 해주세요", "image001"),
Expand Down
1 change: 1 addition & 0 deletions nowait-domain/domain-redis/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ repositories {

dependencies {
implementation project(':nowait-common')
implementation project(':nowait-event')

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package com.nowait.domaincoreredis.common.util;

import java.time.format.DateTimeFormatter;

public class RedisKeyUtils {

// Store rank keys
private static final String KEY_CURRENT = "nowait:store:rank:current";
private static final String KEY_PREVIOUS = "nowait:store:rank:previous";
private static final String KEY_NEXT = "nowait:store:rank:next";

// Menu rank keys
private static final String KEY_FMT = "popular:%d:%s";
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyyMMdd");


private RedisKeyUtils() {
throw new UnsupportedOperationException("유틸리티 서비스는 인스턴스화 할 수 없습니다.");
}
Expand All @@ -21,4 +29,8 @@ public static String buildPreviousKey() {
public static String buildNextKey() {
return KEY_NEXT;
}

public static String buildMenuKey() { return KEY_FMT; }

public static DateTimeFormatter buildMenuDateKey() { return DTF; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nowait.domaincoreredis.rank.exception;

import com.nowait.common.exception.ErrorMessage;

public class MenuCounterUpdateException extends RuntimeException {
public MenuCounterUpdateException() { super(ErrorMessage.MENU_COUNTER_UPDATE.getMessage()); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.nowait.domaincoreredis.rank.listener;

import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import com.nowait.domaincoreredis.rank.service.MenuCounterService;
import com.nowait.nowaitevent.order.event.CookingCompleteEvent;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class CookingCompleteListener {
private final MenuCounterService menuCounterService;

@TransactionalEventListener(
classes = CookingCompleteEvent.class,
phase = TransactionPhase.AFTER_COMMIT
)
public void onCookingComplete(CookingCompleteEvent event) {
Long storeId = event.getStoreId();
for (CookingCompleteEvent.Item item : event.getItems()) {
menuCounterService.incrementMenuCounter(
item.getMenuId(),
storeId,
item.getQuantity()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nowait.domaincoreredis.rank.service;

import java.util.Set;

import org.springframework.data.redis.core.ZSetOperations;

public interface MenuCounterService {

void incrementMenuCounter(Long menuId, Long storeId, int qty);

Set<ZSetOperations.TypedTuple<String>> getTopMenus(Long storeId, int topN);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.nowait.domaincoreredis.rank.service;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import com.nowait.domaincoreredis.common.util.RedisKeyUtils;
import com.nowait.domaincoreredis.rank.exception.MenuCounterUpdateException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
public class MenuCounterServiceImpl implements MenuCounterService {

private final String KEY_FMT = RedisKeyUtils.buildMenuKey();
private final DateTimeFormatter DTF = RedisKeyUtils.buildMenuDateKey();
private final RedisTemplate<String, String> redis;

@Override
public void incrementMenuCounter(Long menuId, Long storeId, int qty) {

try {
String date = LocalDate.now().format(DTF);
String key = String.format(KEY_FMT, storeId, date);

redis.opsForZSet().incrementScore(key, menuId.toString(), qty);

Long expirationTime = redis.getExpire(key);
if (expirationTime == null || expirationTime < 0) {
long secondsUntilMidnight = Duration.between(
LocalDateTime.now(),
LocalDate.now().plusDays(1).atStartOfDay()
).getSeconds();

redis.expire(key, Duration.ofSeconds(secondsUntilMidnight));
}

} catch (Exception e) {
log.error("Failed to increment menu counter for menuId: {}, storeId: {}", + menuId, storeId, e);
throw new MenuCounterUpdateException();
}
}

@Override
public Set<ZSetOperations.TypedTuple<String>> getTopMenus(Long storeId, int topN) {
String date = LocalDate.now().format(DTF);
String key = String.format(KEY_FMT, storeId, date);

return redis.opsForZSet().reverseRangeWithScores(key, 0, topN - 1);
}
}
Loading