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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.campustable.be.domain.menu.dto.MenuRequest;
import com.campustable.be.domain.menu.dto.MenuResponse;
import com.campustable.be.domain.menu.dto.MenuUpdateRequest;
import com.campustable.be.domain.menu.dto.TopMenuResponse;
import com.campustable.be.domain.menu.service.MenuService;
import com.campustable.be.global.aop.LogMonitoringInvocation;
import jakarta.validation.Valid;
Expand All @@ -21,47 +22,49 @@
public class MenuController implements MenuControllerDocs {


private final MenuService menuService;
private final MenuService menuService;


@Override
@GetMapping("/menus")
@LogMonitoringInvocation
public ResponseEntity<List<MenuResponse>> getAllMenus(){

List<MenuResponse> menus = menuService.getAllMenus();
List<MenuResponse> menus = menuService.getAllMenus();

return ResponseEntity.ok(menus);
return ResponseEntity.ok(menus);

}
}

@Override
@Override
@LogMonitoringInvocation
@GetMapping("/menus/{menuId}")
public ResponseEntity<MenuResponse> getMenuById(@PathVariable Long menuId){
return ResponseEntity.ok(menuService.getMenuById(menuId));
}


@Override
@LogMonitoringInvocation
@GetMapping("/category/{category_id}/menus")
public ResponseEntity<List<MenuResponse>> getAllMenusByCategoryId(
@PathVariable(name = "category_id") Long categoryId){

List<MenuResponse> menus = menuService.getAllMenusByCategory(categoryId);
List<MenuResponse> menus = menuService.getAllMenusByCategory(categoryId);

return ResponseEntity.ok(menus);
return ResponseEntity.ok(menus);

}
}

@Override
@LogMonitoringInvocation
@GetMapping("/menus/{menuId}")
public ResponseEntity<MenuResponse> getMenuById(@PathVariable Long menuId){
return ResponseEntity.ok(menuService.getMenuById(menuId));
}

@Override
@LogMonitoringInvocation
@GetMapping("/menus/cafeteria/{cafeteria-id}")
public ResponseEntity<List<MenuResponse>> getAllMenusByCafeteriaId(
@Override
@LogMonitoringInvocation
@GetMapping("/cafeteria/{cafeteria-id}")
public ResponseEntity<List<MenuResponse>> getAllMenusByCafeteriaId(
@PathVariable(name = "cafeteria-id") Long cafeteriaId
) {
return ResponseEntity.ok(menuService.getAllMenusByCafeteriaId(cafeteriaId));
}
) {
return ResponseEntity.ok(menuService.getAllMenusByCafeteriaId(cafeteriaId));
}

@Override
@PostMapping(value = "/admin/menus", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Expand All @@ -71,8 +74,8 @@ public ResponseEntity<MenuResponse> createMenu(
){
MenuResponse createMenu = menuService.createMenu(request, request.getImage());

return ResponseEntity.status(HttpStatus.CREATED).body(createMenu);
}
return ResponseEntity.status(HttpStatus.CREATED).body(createMenu);
}

@Override
@PostMapping(value = "/admin/menus/{menu_id}/image" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Expand All @@ -94,20 +97,30 @@ public ResponseEntity<MenuResponse> updateMenu(
@PathVariable(name = "menu_id") Long menuId,
@RequestBody MenuUpdateRequest updateRequest){

MenuResponse updateMenu = menuService.updateMenu(menuId, updateRequest);
MenuResponse updateMenu = menuService.updateMenu(menuId, updateRequest);

return ResponseEntity.ok(updateMenu);
}
return ResponseEntity.ok(updateMenu);
}

@Override
@LogMonitoringInvocation
@DeleteMapping("/admin/menus/{menu_id}")
public ResponseEntity<Void> deleteMenu(
@PathVariable(name = "menu_id") Long menuId) {

menuService.deleteMenu(menuId);
menuService.deleteMenu(menuId);

return ResponseEntity.noContent().build();
}
return ResponseEntity.noContent().build();
}

@Override
@GetMapping("/cafeteria/{cafeteria-id}/top-menus")
@LogMonitoringInvocation
public ResponseEntity<List<TopMenuResponse>> getTop3MenusByCafeteriaId(@PathVariable(name = "cafeteria-id") Long cafeteriaId) {

List<TopMenuResponse> topMenus = menuService.getTop3MenusByCafeteriaId(cafeteriaId);

return ResponseEntity.ok(topMenus);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.campustable.be.domain.menu.dto.MenuRequest;
import com.campustable.be.domain.menu.dto.MenuResponse;
import com.campustable.be.domain.menu.dto.MenuUpdateRequest;
import com.campustable.be.domain.menu.dto.TopMenuResponse;
import com.campustable.be.global.exception.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand Down Expand Up @@ -136,4 +137,22 @@ ResponseEntity<MenuResponse> updateMenu(
ResponseEntity<Void> deleteMenu(
@Parameter(description = "삭제할 메뉴 ID", example = "1") Long menuId
);

/**
* 특정 식당의 인기 메뉴(Top 3)를 조회합니다.
* Redis ZSet을 기반으로 식당별 주문량이 가장 많은 Top3 메뉴 정보와 랭킹을 반환
* @param cafeteriaId 식당 고유 식별자
* @return 랭킹과 메뉴 상세 정보 리스트를 담은 ResponseEntity
*/
@Operation(summary = "식당별 인기 메뉴 조회(Top 3)", description = "특정 식당 ID의 Top 3 인기 메뉴를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "해당 식당을 찾을 수 없습니다.",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
ResponseEntity<List<TopMenuResponse>> getTop3MenusByCafeteriaId(
@Parameter(description = "조회할 식당 ID",example = "1") Long cafeteriaId
);


}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public Menu toEntity(Category category) {
.menuUrl(null)
.available(this.getAvailable())
.stockQuantity(this.getStockQuantity())

.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class MenuResponse {
private Boolean available;
private Integer stockQuantity;
private LocalDateTime createdDate;
private Long cafeteriaId;

public static MenuResponse from(Menu menu) {
return new MenuResponse(
Expand All @@ -34,7 +35,8 @@ public static MenuResponse from(Menu menu) {
menu.getMenuUrl(),
menu.getAvailable(),
menu.getStockQuantity(),
menu.getCreatedAt()
menu.getCreatedAt(),
menu.getCategory().getCafeteria().getCafeteriaId()
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.campustable.be.domain.menu.dto;

import com.campustable.be.domain.menu.entity.Menu;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TopMenuResponse {

private Long rank;
private MenuResponse menu;

public static TopMenuResponse of(Long rank, Menu menu) {
return TopMenuResponse.builder()
.rank(rank)
.menu(MenuResponse.from(menu))
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
import com.campustable.be.domain.menu.dto.MenuRequest;
import com.campustable.be.domain.menu.dto.MenuResponse;
import com.campustable.be.domain.menu.dto.MenuUpdateRequest;
import com.campustable.be.domain.menu.dto.TopMenuResponse;
import com.campustable.be.domain.menu.entity.Menu;
import com.campustable.be.domain.menu.repository.MenuRepository;
import com.campustable.be.domain.s3.service.S3Service;
import com.campustable.be.global.exception.CustomException;
import com.campustable.be.global.exception.ErrorCode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -31,6 +39,7 @@ public class MenuService {
private final MenuRepository menuRepository;
private final CategoryRepository categoryRepository;
private final CafeteriaService cafeteriaService;
private final StringRedisTemplate stringRedisTemplate;
private final S3Service s3Service;


Expand All @@ -52,12 +61,22 @@ public MenuResponse createMenu(MenuRequest request, MultipartFile image) {
Menu menu = request.toEntity(category);
Menu savedMenu = menuRepository.save(menu);

try {
Long cafeteriaId = savedMenu.getCategory().getCafeteria().getCafeteriaId();
String key = "cafeteria:" + cafeteriaId + ":menu:rank";

stringRedisTemplate.opsForZSet().add(key, String.valueOf(savedMenu.getId()), 0.0);
} catch (Exception e) {

log.error("메뉴 생성 후 Redis 랭킹 등록 실패. (Menu Id: {}), 원인 :{}", savedMenu.getId(), e.getMessage());

}

if (image != null && !image.isEmpty()) {
return uploadMenuImage(savedMenu.getId(), image);
}

return MenuResponse.from(savedMenu);

}

@Transactional
Expand Down Expand Up @@ -96,6 +115,7 @@ public MenuResponse uploadMenuImage(Long menuId, MultipartFile image) {
}
}


return MenuResponse.from(savedMenu);
}

Expand Down Expand Up @@ -192,5 +212,51 @@ public void deleteMenu(Long menuId) {
menuRepository.deleteById(menuId);
}


@Transactional
public List<TopMenuResponse> getTop3MenusByCafeteriaId(Long cafeteriaId) {

cafeteriaService.findCafeteriaById(cafeteriaId);

String key = "cafeteria:" + cafeteriaId + ":menu:rank";

Set<String> topMenus = stringRedisTemplate.opsForZSet().reverseRange(key, 0, 2);

if (topMenus == null || topMenus.isEmpty()) {
return List.of();
}

List<Long> topMenuIds = topMenus.stream()
.map(id -> {
try {
return Long.parseLong(id);
} catch (NumberFormatException e) {
log.warn("Redis에 잘못된 메뉴 ID 형식: {}", id);
return null;
}
})
.filter(Objects::nonNull)
.toList();

List<Menu> menus = menuRepository.findAllById(topMenuIds);

Map<Long, Menu> topMenusMap = menus.stream()
.collect(Collectors.toMap(Menu::getId, Function.identity()));

List<TopMenuResponse> topMenusResponse = new ArrayList<>();

for (int i = 0; i < topMenuIds.size(); i++) {
Long topMenuId = topMenuIds.get(i);
Menu menu = topMenusMap.get(topMenuId);

if (menu != null) {
topMenusResponse.add(TopMenuResponse.of((long) (i + 1), menu));
}
}

return topMenusResponse;

}

}

Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.campustable.be.domain.order.service;

import com.campustable.be.domain.cafeteria.entity.Cafeteria;
import com.campustable.be.domain.cart.entity.Cart;
import com.campustable.be.domain.cart.repository.CartRepository;
import com.campustable.be.domain.category.entity.Category;
import com.campustable.be.domain.menu.entity.Menu;
import com.campustable.be.domain.menu.repository.MenuRepository;
import com.campustable.be.domain.order.dto.OrderResponse;
import com.campustable.be.domain.order.entity.Order;
import com.campustable.be.domain.order.entity.OrderItem;
Expand All @@ -16,8 +19,12 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Service
@RequiredArgsConstructor
Expand All @@ -29,6 +36,7 @@ public class OrderService {
private final CartRepository cartRepository;
private final UserRepository userRepository;
private final OrderItemRepository orderItemRepository;
private final StringRedisTemplate stringRedisTemplate;

public OrderResponse createOrder() {
Long userId = SecurityUtil.getCurrentUserId();
Expand Down Expand Up @@ -63,9 +71,35 @@ public OrderResponse createOrder() {
user.setCart(null);
cartRepository.delete(cart);

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
updateMenuRanking(orderItems);
}
});

return OrderResponse.from(order);
}

private void updateMenuRanking(List<OrderItem> orderItems) {
for(OrderItem orderItem : orderItems) {
try {
Menu menu = orderItem.getMenu();
Category category = menu.getCategory();
Cafeteria cafeteria = category.getCafeteria();
Long cafeteriaId = cafeteria.getCafeteriaId();

String key = "cafeteria:" + cafeteriaId + ":menu:rank";

stringRedisTemplate.opsForZSet()
.incrementScore(key, String.valueOf(menu.getId()), orderItem.getQuantity());
}
catch (Exception e) {
log.error("랭킹 점수 반영 실패: {}",e.getMessage());
}
}
}

public void updateCategoryToReady(Long orderId,Long categoryId) {

List<OrderItem> items = orderItemRepository.findByOrderOrderIdAndCategoryId(orderId, categoryId);
Expand Down