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 @@ -26,9 +26,8 @@ public class PurchaseOrderRepositoryImpl implements PurchaseOrderRepositoryCusto
private final JPAQueryFactory queryFactory;

@Override
public Page<PurchaseOrder> findByStoreIdWithFilters(Long storeId, PurchaseOrderSearchRequest searchRequest,
Pageable pageable) {
// 공통 where 조건
public Page<PurchaseOrder> findByStoreIdWithFilters(Long storeId, PurchaseOrderSearchRequest searchRequest, Pageable pageable) {

BooleanExpression[] whereConditions = {
purchaseOrder.store.storeId.eq(storeId),
statusEq(searchRequest.status()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.validation.Valid;
import kr.inventory.domain.auth.security.CustomUserDetails;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderCreateRequest;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderSearchRequest;
import kr.inventory.domain.sales.controller.dto.response.SalesOrderResponse;
import kr.inventory.domain.sales.service.SalesOrderService;
import kr.inventory.global.common.PageResponse;
Expand Down Expand Up @@ -49,11 +50,13 @@ public ResponseEntity<SalesOrderResponse> createOrder(
public ResponseEntity<PageResponse<SalesOrderResponse>> getStoreOrders(
@AuthenticationPrincipal CustomUserDetails principal,
@PathVariable UUID storePublicId,
@Valid @ModelAttribute SalesOrderSearchRequest request,
@PageableDefault(size = 20) Pageable pageable
) {
PageResponse<SalesOrderResponse> response = salesOrderService.getStoreOrders(
principal.getUserId(),
storePublicId,
request,
pageable
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.inventory.domain.sales.controller.dto.request;

import jakarta.validation.constraints.NotNull;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

jakarta.validation.constraints.NotNull import가 사용되지 않고 있습니다. 레포지토리 스타일 가이드의 "import 정리 및 unused 제거" 규칙(66번 라인)에 따라 불필요한 import는 제거하는 것이 좋습니다.

References
  1. 프로젝트 컨벤션에 따라 사용하지 않는 import는 제거해야 합니다. (link)

import kr.inventory.domain.sales.entity.enums.SalesOrderStatus;

import java.math.BigDecimal;
import java.time.OffsetDateTime;

public record SalesOrderSearchRequest(
OffsetDateTime from,
OffsetDateTime to,
SalesOrderStatus status,
BigDecimal amountMin,
BigDecimal amountMax
) {}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public enum SalesOrderErrorCode implements ErrorModel {
INSUFFICIENT_STOCK(HttpStatus.CONFLICT, "SO008","재고가 부족합니다."),
ORDER_NOT_REFUNDABLE(HttpStatus.CONFLICT, "SO009","환불할 수 없는 주문입니다."),
ORDER_ALREADY_REFUNDED(HttpStatus.CONFLICT, "SO010","이미 환불된 주문입니다."),
INVALID_SALES_LEDGER_PERIOD(HttpStatus.BAD_REQUEST, "SO011", "매출 내역 조회 기간이 올바르지 않습니다.");
INVALID_SEARCH_PERIOD(HttpStatus.BAD_REQUEST, "SO011", "조회 기간이 올바르지 않습니다."),
INVALID_AMOUNT_RANGE(HttpStatus.BAD_REQUEST, "SO012", "금액 범위가 유효하지 않습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.inventory.domain.sales.repository;

import kr.inventory.domain.sales.controller.dto.request.SalesOrderSearchRequest;
import kr.inventory.domain.sales.controller.dto.response.SalesLedgerTotalSummaryResponse;
import kr.inventory.domain.sales.entity.SalesOrder;
import kr.inventory.domain.sales.entity.enums.SalesOrderStatus;
Expand All @@ -17,7 +18,11 @@ public interface SalesOrderRepositoryCustom {

Optional<SalesOrder> findByOrderPublicIdWithItems(UUID orderPublicId, Long storeId);

Page<SalesOrder> findStoreOrders(Long storeId, Pageable pageable);
Page<SalesOrder> findStoreOrders(
Long storeId,
SalesOrderSearchRequest request,
Pageable pageable
);

Page<SalesOrder> findSalesLedgerOrders(
Long storeId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.LockModeType;
import kr.inventory.domain.dining.entity.QDiningTable;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderSearchRequest;
import kr.inventory.domain.sales.controller.dto.response.SalesLedgerTotalSummaryResponse;
import kr.inventory.domain.sales.entity.QSalesOrderItem;
import kr.inventory.domain.sales.entity.SalesOrder;
Expand All @@ -23,10 +24,7 @@
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.*;

import static kr.inventory.domain.sales.entity.QSalesOrder.salesOrder;
import static kr.inventory.domain.sales.entity.QSalesOrderItem.salesOrderItem;
Expand All @@ -52,6 +50,7 @@ public Optional<SalesOrder> findByIdAndStoreStoreIdWithLock(Long salesOrderId, L
);
}

// 주문 현황
@Override
public Optional<SalesOrder> findByOrderPublicIdWithItems(UUID orderPublicId, Long storeId) {
QDiningTable diningTable = QDiningTable.diningTable;
Expand All @@ -69,11 +68,14 @@ public Optional<SalesOrder> findByOrderPublicIdWithItems(UUID orderPublicId, Lon
}

@Override
public Page<SalesOrder> findStoreOrders(Long storeId, Pageable pageable) {
public Page<SalesOrder> findStoreOrders(Long storeId, SalesOrderSearchRequest request, Pageable pageable) {
QDiningTable diningTable = QDiningTable.diningTable;
BooleanExpression[] conditions = orderConditions(storeId, request);

List<SalesOrder> orders = queryFactory
.selectFrom(salesOrder)
.leftJoin(salesOrder.diningTable).fetchJoin()
.where(salesOrder.store.storeId.eq(storeId))
.leftJoin(salesOrder.diningTable, diningTable).fetchJoin()
.where(conditions)
.orderBy(salesOrder.orderedAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
Expand All @@ -82,13 +84,14 @@ public Page<SalesOrder> findStoreOrders(Long storeId, Pageable pageable) {
Long totalCount = queryFactory
.select(salesOrder.count())
.from(salesOrder)
.where(salesOrder.store.storeId.eq(storeId))
.where(conditions)
.fetchOne();

long safeTotalCount = totalCount == null ? 0L : totalCount;
return new PageImpl<>(orders, pageable, safeTotalCount);
}

// 매출 내역
@Override
public Page<SalesOrder> findSalesLedgerOrders(
Long storeId,
Expand Down Expand Up @@ -192,6 +195,23 @@ public SalesLedgerTotalSummaryResponse calculateSalesLedgerSummary(Long storeId,
return new SalesLedgerTotalSummaryResponse(count, totalAmount, totalRefundAmount, totalNetAmount);
}

private BooleanExpression[] orderConditions(Long storeId, SalesOrderSearchRequest request) {
List<BooleanExpression> conditions = new ArrayList<>();
conditions.add(salesOrder.store.storeId.eq(storeId));

if (request.from() != null) {
conditions.add(salesOrder.orderedAt.goe(request.from().withOffsetSameInstant(ZoneOffset.UTC)));
}
if (request.to() != null) {
conditions.add(salesOrder.orderedAt.loe(request.to().withOffsetSameInstant(ZoneOffset.UTC)));
}

conditions.add(salesOrderStatusEq(request.status()));
conditions.add(totalAmountGoe(request.amountMin()));
conditions.add(totalAmountLoe(request.amountMax()));
return conditions.stream().filter(Objects::nonNull).toArray(BooleanExpression[]::new);
}

private BooleanExpression[] ledgerConditions(Long storeId, SalesLedgerQueryCondition condition) {
List<BooleanExpression> conditions = new ArrayList<>();
conditions.add(salesOrder.store.storeId.eq(storeId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,13 @@ private PageResponse<SalesLedgerOrderSummaryResponse> toLedgerOrderPage(

private void validateSearchPeriod(OffsetDateTime from, OffsetDateTime to) {
if (from.isAfter(to)) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_SALES_LEDGER_PERIOD);
throw new SalesOrderException(SalesOrderErrorCode.INVALID_SEARCH_PERIOD);
}
}

private void validateAmountRange(BigDecimal amountMin, BigDecimal amountMax) {
if (amountMin != null && amountMax != null && amountMin.compareTo(amountMax) > 0) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_SALES_LEDGER_PERIOD);
throw new SalesOrderException(SalesOrderErrorCode.INVALID_AMOUNT_RANGE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import kr.inventory.domain.reference.repository.MenuRepository;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderCreateRequest;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderItemRequest;
import kr.inventory.domain.sales.controller.dto.request.SalesOrderSearchRequest;
import kr.inventory.domain.sales.controller.dto.response.SalesOrderResponse;
import kr.inventory.domain.sales.entity.SalesOrder;
import kr.inventory.domain.sales.entity.SalesOrderItem;
Expand Down Expand Up @@ -155,12 +156,14 @@ public SalesOrderResponse getOrder(UUID orderPublicId, Long userId, UUID storePu
return SalesOrderResponse.from(order, items);
}

public PageResponse<SalesOrderResponse> getStoreOrders(Long userId, UUID storePublicId, Pageable pageable) {
public PageResponse<SalesOrderResponse> getStoreOrders(Long userId, UUID storePublicId, SalesOrderSearchRequest request, Pageable pageable) {
validateSearchPeriod(request.from(), request.to());
validateAmountRange(request.amountMin(), request.amountMax());

Long storeId = storeAccessValidator.validateAndGetStoreId(userId, storePublicId);

Page<SalesOrder> page = salesOrderRepository.findStoreOrders(storeId, pageable);
Page<SalesOrder> page = salesOrderRepository.findStoreOrders(storeId, request, pageable);

// N+1 방지: 모든 주문 ID를 한번에 조회
List<Long> orderIds = page.getContent().stream()
.map(SalesOrder::getSalesOrderId)
.toList();
Expand All @@ -169,11 +172,9 @@ public PageResponse<SalesOrderResponse> getStoreOrders(Long userId, UUID storePu
? Collections.emptyList()
: salesOrderItemRepository.findBySalesOrderSalesOrderIdIn(orderIds);

// 주문 ID별로 항목 그룹핑
Map<Long, List<SalesOrderItem>> itemsByOrderId = allItems.stream()
.collect(Collectors.groupingBy(item -> item.getSalesOrder().getSalesOrderId()));

// PageResponse로 변환
Page<SalesOrderResponse> responsePage = page.map(order -> {
List<SalesOrderItem> items = itemsByOrderId.getOrDefault(
order.getSalesOrderId(),
Expand Down Expand Up @@ -217,48 +218,17 @@ public SalesOrderResponse refundOrder(UUID orderPublicId, Long userId, UUID stor
return SalesOrderResponse.from(order, items);
}

/**
* 재료별 필요 수량 계산
*/
private Map<Long, BigDecimal> calculateIngredientUsage(List<SalesOrderItem> items) {
Map<Long, BigDecimal> usageMap = new HashMap<>();

for (SalesOrderItem item : items) {
Menu menu = item.getMenu();
JsonNode ingredientsJson = menu.getIngredientsJson();

if (ingredientsJson == null || ingredientsJson.isEmpty()) {
continue;
}

if (ingredientsJson.has("ingredients") && ingredientsJson.get("ingredients").isArray()) {
for (JsonNode ingredientNode : ingredientsJson.get("ingredients")) {
// ingredientId 검증
if (!ingredientNode.has("ingredientId")) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_INGREDIENT_DATA);
}

// quantity 검증 및 변환
if (!ingredientNode.has("quantity")) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_INGREDIENT_DATA);
}

Long ingredientId = ingredientNode.get("ingredientId").asLong();

BigDecimal quantity;
try {
quantity = new BigDecimal(ingredientNode.get("quantity").asText());
} catch (NumberFormatException e) {
// 잘못된 형식의 재료 수량 데이터
throw new SalesOrderException(SalesOrderErrorCode.INVALID_INGREDIENT_DATA);
}

BigDecimal totalQuantity = quantity.multiply(BigDecimal.valueOf(item.getQuantity()));
usageMap.merge(ingredientId, totalQuantity, BigDecimal::add);
}
}
private void validateSearchPeriod(OffsetDateTime from, OffsetDateTime to) {
if (from != null && to != null && from.isAfter(to)) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_SEARCH_PERIOD);
}
}

return usageMap;
private void validateAmountRange(BigDecimal amountMin, BigDecimal amountMax) {
if (amountMin != null && amountMax != null && amountMin.compareTo(amountMax) > 0) {
throw new SalesOrderException(SalesOrderErrorCode.INVALID_AMOUNT_RANGE);
}
}
Comment on lines +221 to 231

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

validateSearchPeriodvalidateAmountRange 메서드는 SalesLedgerService에도 동일하게 존재합니다. 이는 코드 중복으로, 레포지토리 스타일 가이드의 DRY 원칙(13, 34, 89번 라인)에 위배됩니다. 이 로직을 별도의 유효성 검사 컴포넌트(예: SearchRequestValidator)로 추출하여 공통으로 사용하면 유지보수성을 높일 수 있습니다.

References
  1. 중복된 로직은 공통화하여 코드의 재사용성을 높이고 유지보수 비용을 줄여야 합니다. (link)



}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kr.inventory.domain.sales.controller;

import kr.inventory.domain.auth.security.CustomUserDetails;
import kr.inventory.domain.sales.controller.dto.request.SalesLedgerSearchRequest;
import kr.inventory.domain.sales.controller.dto.response.SalesLedgerOrderDetailResponse;
import kr.inventory.domain.sales.controller.dto.response.SalesLedgerOrderSummaryResponse;
import kr.inventory.domain.sales.entity.enums.SalesOrderStatus;
Expand Down Expand Up @@ -69,7 +70,7 @@ void givenSearchRequest_whenGetSalesLedgerOrders_thenReturn200() throws Exceptio
new PageImpl<>(List.of(content), PageRequest.of(0, 20), 1)
);

given(salesLedgerService.getSalesLedgerOrders(eq(userId), eq(storePublicId), any(), any())).willReturn(response);
given(salesLedgerService.getSalesLedgerOrders(eq(userId), eq(storePublicId), any(SalesLedgerSearchRequest.class), any(PageRequest.class))).willReturn(response);

mockMvc.perform(get("/api/sales/{storePublicId}/orders", storePublicId)
.with(csrf())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ void givenStorePublicId_whenGetStoreOrders_thenReturn200() throws Exception {
new PageImpl<>(List.of(response), PageRequest.of(0, 20), 1)
);

given(salesOrderService.getStoreOrders(eq(userId), eq(storePublicId), any()))
given(salesOrderService.getStoreOrders(eq(userId), eq(storePublicId), any(), any()))
.willReturn(pageResponse);

// when & then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ void givenInvalidPeriod_whenGetSalesLedgerOrders_thenThrowException() {
assertThatThrownBy(() -> salesLedgerService.getSalesLedgerOrders(userId, storePublicId, request, PageRequest.of(0, 20)))
.isInstanceOf(SalesOrderException.class)
.extracting("errorModel")
.isEqualTo(SalesOrderErrorCode.INVALID_SALES_LEDGER_PERIOD);
.isEqualTo(SalesOrderErrorCode.INVALID_SEARCH_PERIOD);
}

@Test
Expand Down
Loading
Loading