diff --git a/src/main/java/com/almang/inventory/global/api/SuccessMessage.java b/src/main/java/com/almang/inventory/global/api/SuccessMessage.java index a79e985f..305e4b73 100644 --- a/src/main/java/com/almang/inventory/global/api/SuccessMessage.java +++ b/src/main/java/com/almang/inventory/global/api/SuccessMessage.java @@ -41,6 +41,7 @@ public enum SuccessMessage { GET_ORDER_TEMPLATE_DETAIL("발주 템플릿 상세 조회 성공"), CREATE_ORDER_SUCCESS("발주 생성 성공"), GET_ORDER_SUCCESS("발주 조회 성공"), + GET_ORDER_LIST_SUCCESS("발주 목록 조회 성공"), ; private final String message; diff --git a/src/main/java/com/almang/inventory/order/controller/OrderController.java b/src/main/java/com/almang/inventory/order/controller/OrderController.java index 5237f590..03ffb835 100644 --- a/src/main/java/com/almang/inventory/order/controller/OrderController.java +++ b/src/main/java/com/almang/inventory/order/controller/OrderController.java @@ -1,14 +1,17 @@ package com.almang.inventory.order.controller; import com.almang.inventory.global.api.ApiResponse; +import com.almang.inventory.global.api.PageResponse; import com.almang.inventory.global.api.SuccessMessage; import com.almang.inventory.global.security.principal.CustomUserPrincipal; +import com.almang.inventory.order.domain.OrderStatus; import com.almang.inventory.order.dto.request.CreateOrderRequest; import com.almang.inventory.order.dto.response.OrderResponse; import com.almang.inventory.order.service.OrderService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.time.LocalDate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -18,6 +21,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j @@ -58,4 +62,26 @@ public ResponseEntity> getOrder( ApiResponse.success(SuccessMessage.GET_ORDER_SUCCESS.getMessage(), response) ); } + + @GetMapping + @Operation(summary = "발주 목록 조회", description = "발주 목록을 페이지네이션, 발주처, 상태, 날짜 검색 조건과 함께 조회합니다.") + public ResponseEntity>> getOrderList( + @AuthenticationPrincipal CustomUserPrincipal userPrincipal, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "size", required = false) Integer size, + @RequestParam(value = "vendorId", required = false) Long vendorId, + @RequestParam(value = "orderStatus", required = false) OrderStatus status, + @RequestParam(value = "fromDate", required = false) LocalDate fromDate, + @RequestParam(value = "endDate", required = false) LocalDate endDate + ) { + Long userId = userPrincipal.getId(); + log.info("[OrderController] 발주 목록 조회 요청 - userId: {}, page: {}, size: {}, vendorId: {}, status: {}, fromDate: {}, endDate: {}", + userId, page, size, vendorId, status, fromDate, endDate); + PageResponse response = + orderService.getOrderList(userId, vendorId, page, size, status, fromDate, endDate); + + return ResponseEntity.ok( + ApiResponse.success(SuccessMessage.GET_ORDER_LIST_SUCCESS.getMessage(), response) + ); + } } diff --git a/src/main/java/com/almang/inventory/order/repository/OrderRepository.java b/src/main/java/com/almang/inventory/order/repository/OrderRepository.java index c9c1456c..c73e481d 100644 --- a/src/main/java/com/almang/inventory/order/repository/OrderRepository.java +++ b/src/main/java/com/almang/inventory/order/repository/OrderRepository.java @@ -9,14 +9,23 @@ public interface OrderRepository extends JpaRepository { - // 상점 기준 발주 목록 - Page findAllByStoreId(Long storeId, Pageable pageable); - - // 상점 + 상태 기준 - Page findAllByStoreIdAndStatus(Long storeId, OrderStatus status, Pageable pageable); - - // 기간 + 상점 기준 + // 필터 없음 Page findAllByStoreIdAndCreatedAtBetween( Long storeId, LocalDateTime start, LocalDateTime end, Pageable pageable ); + + // 상태 필터 + Page findAllByStoreIdAndStatusAndCreatedAtBetween( + Long storeId, OrderStatus status, LocalDateTime start, LocalDateTime end, Pageable pageable + ); + + // 발주처 필터 + Page findAllByStoreIdAndVendorIdAndCreatedAtBetween( + Long storeId, Long vendorId, LocalDateTime start, LocalDateTime end, Pageable pageable + ); + + // 발주처 + 상태 필터 + Page findAllByStoreIdAndVendorIdAndStatusAndCreatedAtBetween( + Long storeId, Long vendorId, OrderStatus status, LocalDateTime start, LocalDateTime end, Pageable pageable + ); } diff --git a/src/main/java/com/almang/inventory/order/service/OrderService.java b/src/main/java/com/almang/inventory/order/service/OrderService.java index d2afe585..baa41e41 100644 --- a/src/main/java/com/almang/inventory/order/service/OrderService.java +++ b/src/main/java/com/almang/inventory/order/service/OrderService.java @@ -1,7 +1,9 @@ package com.almang.inventory.order.service; +import com.almang.inventory.global.api.PageResponse; import com.almang.inventory.global.exception.BaseException; import com.almang.inventory.global.exception.ErrorCode; +import com.almang.inventory.global.util.PaginationUtil; import com.almang.inventory.order.domain.Order; import com.almang.inventory.order.domain.OrderItem; import com.almang.inventory.order.domain.OrderStatus; @@ -19,10 +21,14 @@ import com.almang.inventory.vendor.domain.Vendor; import com.almang.inventory.vendor.repository.VendorRepository; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -69,6 +75,23 @@ public OrderResponse getOrder(Long orderId, Long userId) { return OrderResponse.of(order, order.getItems()); } + @Transactional(readOnly = true) + public PageResponse getOrderList( + Long userId, Long vendorId, Integer page, Integer size, + OrderStatus status, LocalDate fromDate, LocalDate toDate + ) { + User user = findUserById(userId); + Store store = user.getStore(); + + log.info("[OrderService] 발주 목록 조회 요청 - userId: {}, storeId: {}", userId, store.getId()); + PageRequest pageable = PaginationUtil.createPageRequest(page, size, "createdAt"); + Page orderPage = findOrdersByFilter(store.getId(), vendorId, status, fromDate, toDate, pageable); + Page mapped = orderPage.map(order -> OrderResponse.of(order, order.getItems())); + + log.info("[OrderService] 발주 목록 조회 성공 - userId: {}, storeId: {}", userId, store.getId()); + return PageResponse.from(mapped); + } + private List createOrderItems(List requests, Store store) { List items = new ArrayList<>(); @@ -157,4 +180,43 @@ private Order findOrderByIdAndValidateAccess(Long orderId, Store store) { } return order; } + + private Page findOrdersByFilter( + Long storeId, Long vendorId, OrderStatus status, LocalDate fromDate, LocalDate toDate, Pageable pageable + ) { + LocalDate startDate = fromDate != null ? fromDate : LocalDate.of(1970, 1, 1); + LocalDateTime start = startDate.atStartOfDay(); + + LocalDate endDate = toDate != null ? toDate : LocalDate.now(); + LocalDateTime end = endDate.plusDays(1).atStartOfDay().minusNanos(1); + + boolean hasVendor = vendorId != null; + boolean hasStatus = status != null; + + // 1) 필터 없음 + if (!hasVendor && !hasStatus) { + return orderRepository.findAllByStoreIdAndCreatedAtBetween( + storeId, start, end, pageable + ); + } + + // 2) 상태 필터 + if (!hasVendor) { + return orderRepository.findAllByStoreIdAndStatusAndCreatedAtBetween( + storeId, status, start, end, pageable + ); + } + + // 3) 발주처 필터 + if (!hasStatus) { + return orderRepository.findAllByStoreIdAndVendorIdAndCreatedAtBetween( + storeId, vendorId, start, end, pageable + ); + } + + // 4) 발주처 + 상태 필터 + return orderRepository.findAllByStoreIdAndVendorIdAndStatusAndCreatedAtBetween( + storeId, vendorId, status, start, end, pageable + ); + } } diff --git a/src/test/java/com/almang/inventory/order/controller/OrderControllerTest.java b/src/test/java/com/almang/inventory/order/controller/OrderControllerTest.java index 866ad66c..2dc04ea5 100644 --- a/src/test/java/com/almang/inventory/order/controller/OrderControllerTest.java +++ b/src/test/java/com/almang/inventory/order/controller/OrderControllerTest.java @@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import com.almang.inventory.global.api.PageResponse; import com.almang.inventory.global.api.SuccessMessage; import com.almang.inventory.global.config.TestSecurityConfig; import com.almang.inventory.global.exception.BaseException; @@ -314,4 +315,101 @@ private UsernamePasswordAuthenticationToken auth() { .andExpect(jsonPath("$.message").value(ErrorCode.ORDER_ACCESS_DENIED.getMessage())) .andExpect(jsonPath("$.data").doesNotExist()); } + + @Test + void 발주_목록_조회에_성공한다() throws Exception { + // given + OrderResponse r1 = new OrderResponse( + 1L, + 10L, + 10L, + "메시지1", + OrderStatus.REQUEST, + 1, + LocalDate.now().plusDays(1), + null, + null, + true, + 5000, + List.of() + ); + + OrderResponse r2 = new OrderResponse( + 2L, + 10L, + 10L, + "메시지2", + OrderStatus.REQUEST, + 2, + LocalDate.now().plusDays(2), + null, + null, + true, + 9000, + List.of() + ); + + PageResponse pageResponse = new PageResponse<>( + List.of(r1, r2), + 1, + 20, + 2L, + 1, + true + ); + + when(orderService.getOrderList(anyLong(), any(), any(), any(), any(), any(), any())) + .thenReturn(pageResponse); + + // when & then + mockMvc.perform(get("/api/v1/order") + .param("page", "1") + .param("size", "20") + .with(authentication(auth()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.message") + .value(SuccessMessage.GET_ORDER_LIST_SUCCESS.getMessage())) + .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.content[0].orderId").value(1)) + .andExpect(jsonPath("$.data.content[1].orderId").value(2)); + } + + @Test + void 발주_목록_조회시_사용자가_존재하지_않으면_예외가_발생한다() throws Exception { + // given + when(orderService.getOrderList(anyLong(), any(), any(), any(), any(), any(), any())) + .thenThrow(new BaseException(ErrorCode.USER_NOT_FOUND)); + + // when & then + mockMvc.perform(get("/api/v1/order") + .param("page", "1") + .param("size", "20") + .with(authentication(auth()))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.USER_NOT_FOUND.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.USER_NOT_FOUND.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + void 발주_목록_조회시_상점_접근_권한이_없으면_예외가_발생한다() throws Exception { + // given + when(orderService.getOrderList(anyLong(), any(), any(), any(), any(), any(), any())) + .thenThrow(new BaseException(ErrorCode.ORDER_ACCESS_DENIED)); + + // when & then + mockMvc.perform(get("/api/v1/order") + .param("page", "1") + .param("size", "20") + .with(authentication(auth()))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status") + .value(ErrorCode.ORDER_ACCESS_DENIED.getHttpStatus().value())) + .andExpect(jsonPath("$.message") + .value(ErrorCode.ORDER_ACCESS_DENIED.getMessage())) + .andExpect(jsonPath("$.data").doesNotExist()); + } } diff --git a/src/test/java/com/almang/inventory/order/service/OrderServiceTest.java b/src/test/java/com/almang/inventory/order/service/OrderServiceTest.java index 73bd24ec..0bea71b9 100644 --- a/src/test/java/com/almang/inventory/order/service/OrderServiceTest.java +++ b/src/test/java/com/almang/inventory/order/service/OrderServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.almang.inventory.global.api.PageResponse; import com.almang.inventory.global.exception.BaseException; import com.almang.inventory.global.exception.ErrorCode; import com.almang.inventory.order.domain.Order; @@ -412,4 +413,196 @@ private Product newProduct(Store store, Vendor vendor, String name, String code) .isInstanceOf(BaseException.class) .hasMessageContaining(ErrorCode.ORDER_ACCESS_DENIED.getMessage()); } + + @Test + void 발주_목록_조회_기본_조회에_성공한다() { + // given + Store store = newStore("테스트 상점"); + User user = newUser(store, "order_list_user"); + Vendor vendor = newVendor(store, "발주처1"); + + Product p1 = newProduct(store, vendor, "상품1", "P001"); + Product p2 = newProduct(store, vendor, "상품2", "P002"); + + CreateOrderRequest req1 = new CreateOrderRequest( + vendor.getId(), + "메시지1", + 1, + List.of(new CreateOrderItemRequest(p1.getId(), 5, 1000, null)) + ); + orderService.createOrder(req1, user.getId()); + + CreateOrderRequest req2 = new CreateOrderRequest( + vendor.getId(), + "메시지2", + 2, + List.of(new CreateOrderItemRequest(p2.getId(), 3, 2000, null)) + ); + orderService.createOrder(req2, user.getId()); + + // when + PageResponse page = orderService.getOrderList( + user.getId(), + null, + 1, + 20, + null, + null, + null + ); + + // then + assertThat(page.totalElements()).isEqualTo(2); + assertThat(page.content()).hasSize(2); + + OrderResponse first = page.content().get(0); + OrderResponse second = page.content().get(1); + + assertThat(first.orderMessage()).isEqualTo("메시지1"); + assertThat(second.orderMessage()).isEqualTo("메시지2"); + } + + @Test + void 발주_목록_조회시_발주처로_필터링된다() { + // given + Store store = newStore("테스트 상점"); + User user = newUser(store, "order_list_user"); + Vendor vendorA = newVendor(store, "A발주처"); + Vendor vendorB = newVendor(store, "B발주처"); + + Product p1 = newProduct(store, vendorA, "상품1", "P001"); + Product p2 = newProduct(store, vendorB, "상품2", "P002"); + + orderService.createOrder( + new CreateOrderRequest( + vendorA.getId(), "A 요청", 1, + List.of(new CreateOrderItemRequest(p1.getId(), 5, 1000, null)) + ), + user.getId() + ); + orderService.createOrder( + new CreateOrderRequest( + vendorB.getId(), "B 요청", 1, + List.of(new CreateOrderItemRequest(p2.getId(), 3, 2000, null)) + ), + user.getId() + ); + + // when + PageResponse page = orderService.getOrderList( + user.getId(), + vendorA.getId(), + 1, + 20, + null, + null, + null + ); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + + OrderResponse first = page.content().get(0); + assertThat(first.vendorId()).isEqualTo(vendorA.getId()); + assertThat(first.orderMessage()).isEqualTo("A 요청"); + } + + @Test + void 발주_목록_조회시_상태로_필터링된다() { + // given + Store store = newStore("테스트 상점"); + User user = newUser(store, "order_list_user"); + Vendor vendor = newVendor(store, "발주처1"); + + Product product = newProduct(store, vendor, "상품1", "P001"); + + orderService.createOrder( + new CreateOrderRequest( + vendor.getId(), "REQUEST 메시지", 1, + List.of(new CreateOrderItemRequest(product.getId(), 1, 1000, null)) + ), + user.getId() + ); + Order saved = orderRepository.save( + Order.builder() + .store(store) + .vendor(vendor) + .status(OrderStatus.IN_PRODUCTION) + .orderMessage("IN PRODUCTION 메시지") + .leadTime(1) + .expectedArrival(LocalDate.now().plusDays(1)) + .activated(true) + .totalPrice(3000) + .build() + ); + + // when + PageResponse page = orderService.getOrderList( + user.getId(), + null, + 1, + 20, + OrderStatus.REQUEST, + null, + null + ); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + + OrderResponse first = page.content().get(0); + assertThat(first.orderStatus()).isEqualTo(OrderStatus.REQUEST); + assertThat(first.orderMessage()).isEqualTo("REQUEST 메시지"); + } + + @Test + void 발주_목록_조회시_날짜_필터링_적용된다() { + // given + Store store = newStore("테스트 상점"); + User user = newUser(store, "order_list_user"); + Vendor vendor = newVendor(store, "발주처1"); + Product product = newProduct(store, vendor, "상품", "P001"); + + orderService.createOrder( + new CreateOrderRequest( + vendor.getId(), "오늘 발주", 1, + List.of(new CreateOrderItemRequest(product.getId(), 1, 1000, null)) + ), + user.getId() + ); + + LocalDate yesterday = LocalDate.now().minusDays(1); + + // when + PageResponse page = orderService.getOrderList( + user.getId(), + null, + 1, + 20, + null, + yesterday, + null + ); + + // then + assertThat(page.totalElements()).isEqualTo(1); + assertThat(page.content()).hasSize(1); + + OrderResponse first = page.content().get(0); + assertThat(first.orderMessage()).isEqualTo("오늘 발주"); + } + + @Test + void 발주_목록_조회시_사용자가_존재하지_않으면_예외가_발생한다() { + // given + Long notExistUserId = 9999L; + + // when & then + assertThatThrownBy(() -> orderService.getOrderList( + notExistUserId, null, 1, 20, null, null, null)) + .isInstanceOf(BaseException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } }