From 34d3a72111adcceda6219765a96a221c72931f12 Mon Sep 17 00:00:00 2001 From: Myeongseok-Kang Date: Sat, 21 Feb 2026 23:02:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A3=BC=EB=AC=B8=20=ED=86=B5=EA=B3=84?= =?UTF-8?q?=20API=20=EB=B0=8F=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/order/OrderController.java | 11 ++++ .../backend/domain/order/OrderRepository.java | 66 ++++++++++++++++++- .../backend/domain/order/OrderService.java | 40 +++++++++++ .../domain/order/dto/OrderStatsResponse.java | 38 +++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 src/main/java/app/dearobjet/backend/domain/order/dto/OrderStatsResponse.java diff --git a/src/main/java/app/dearobjet/backend/domain/order/OrderController.java b/src/main/java/app/dearobjet/backend/domain/order/OrderController.java index 78c9fbb..21a6188 100644 --- a/src/main/java/app/dearobjet/backend/domain/order/OrderController.java +++ b/src/main/java/app/dearobjet/backend/domain/order/OrderController.java @@ -8,6 +8,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; + @RestController @RequiredArgsConstructor @RequestMapping("/api/v1") @@ -79,4 +81,13 @@ public ApiResponse confirmPurchase( orderService.confirmPurchase(userDetails.getUserId(), orderId); return ApiResponse.of(null); } + + @GetMapping("/orders/stats") + public ApiResponse getOrderStats( + @AuthenticationPrincipal CustomUserDetails userDetails, + @RequestParam LocalDate from, + @RequestParam LocalDate to + ) { + return ApiResponse.of(orderService.getOrderStats(userDetails.getUserId(), from, to)); + } } diff --git a/src/main/java/app/dearobjet/backend/domain/order/OrderRepository.java b/src/main/java/app/dearobjet/backend/domain/order/OrderRepository.java index 72e88aa..cbbaf1f 100644 --- a/src/main/java/app/dearobjet/backend/domain/order/OrderRepository.java +++ b/src/main/java/app/dearobjet/backend/domain/order/OrderRepository.java @@ -5,8 +5,72 @@ import app.dearobjet.backend.domain.user.entity.User; import org.springframework.data.domain.*; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; public interface OrderRepository extends JpaRepository { + Page findByUser(User user, Pageable pageable); Page findByUserAndStatus(User user, OrderStatus status, Pageable pageable); -} + + // 1) 일별 매출 + @Query(value = """ + SELECT DATE(o.paid_at) AS d, + COUNT(*) AS cnt, + COALESCE(SUM(o.total_amount), 0) AS amt + FROM orders o + WHERE o.user_id = :userId + AND o.paid_at IS NOT NULL + AND o.paid_at BETWEEN :fromDt AND :toDt + AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED') + GROUP BY DATE(o.paid_at) + ORDER BY d ASC + """, nativeQuery = true) + List findSalesDaily( + @Param("userId") Long userId, + @Param("fromDt") LocalDateTime fromDt, + @Param("toDt") LocalDateTime toDt + ); + + // 2) 인기 상품 Top 10 + @Query(value = """ + SELECT oi.item_id AS itemId, + COALESCE(SUM(oi.quantity), 0) AS qty, + COALESCE(SUM(oi.line_amount), 0) AS sales + FROM orders o + JOIN order_item oi ON oi.orders_id = o.orders_id + WHERE o.user_id = :userId + AND o.paid_at IS NOT NULL + AND o.paid_at BETWEEN :fromDt AND :toDt + AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED') + GROUP BY oi.item_id + ORDER BY sales DESC + LIMIT 10 + """, nativeQuery = true) + List findPopularItemsTop10( + @Param("userId") Long userId, + @Param("fromDt") LocalDateTime fromDt, + @Param("toDt") LocalDateTime toDt + ); + + // 3) 시간대별 주문수 + @Query(value = """ + SELECT CAST(EXTRACT(HOUR FROM o.paid_at) AS INT) AS h, + COUNT(*) AS cnt + FROM orders o + WHERE o.user_id = :userId + AND o.paid_at IS NOT NULL + AND o.paid_at BETWEEN :fromDt AND :toDt + AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED') + GROUP BY CAST(EXTRACT(HOUR FROM o.paid_at) AS INT) + ORDER BY h ASC + """, nativeQuery = true) + List findOrdersByHour( + @Param("userId") Long userId, + @Param("fromDt") LocalDateTime fromDt, + @Param("toDt") LocalDateTime toDt + ); +} \ No newline at end of file diff --git a/src/main/java/app/dearobjet/backend/domain/order/OrderService.java b/src/main/java/app/dearobjet/backend/domain/order/OrderService.java index cdc4d29..cf4122f 100644 --- a/src/main/java/app/dearobjet/backend/domain/order/OrderService.java +++ b/src/main/java/app/dearobjet/backend/domain/order/OrderService.java @@ -12,6 +12,8 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -131,6 +133,44 @@ public void confirmPurchase(Long userId, Long orderId) { order.updateStatus(OrderStatus.COMPLETED); } + @Transactional(readOnly = true) + public OrderStatsResponse getOrderStats(Long userId, LocalDate from, LocalDate to) { + + LocalDateTime fromDt = from.atStartOfDay(); + LocalDateTime toDt = LocalDateTime.of(to, LocalTime.of(23, 59, 59)); + + // 1) 일별 매출 + List salesRows = orderRepository.findSalesDaily(userId, fromDt, toDt); + List sales = new ArrayList<>(); + for (Object[] row : salesRows) { + String date = String.valueOf(row[0]); + long cnt = ((Number) row[1]).longValue(); + long amt = ((Number) row[2]).longValue(); + sales.add(new OrderStatsResponse.SalesPoint(date, cnt, amt)); + } + + // 2) 인기 상품 Top 10 + List popularRows = orderRepository.findPopularItemsTop10(userId, fromDt, toDt); + List popularItems = new ArrayList<>(); + for (Object[] row : popularRows) { + Long itemId = ((Number) row[0]).longValue(); + long qty = ((Number) row[1]).longValue(); + long salesAmt = ((Number) row[2]).longValue(); + popularItems.add(new OrderStatsResponse.PopularItemPoint(itemId, qty, salesAmt)); + } + + // 3) 시간대별 주문수 + List hourRows = orderRepository.findOrdersByHour(userId, fromDt, toDt); + List ordersByHour = new ArrayList<>(); + for (Object[] row : hourRows) { + int hour = ((Number) row[0]).intValue(); + long cnt = ((Number) row[1]).longValue(); + ordersByHour.add(new OrderStatsResponse.OrdersByHourPoint(hour, cnt)); + } + + return new OrderStatsResponse(sales, popularItems, ordersByHour); + } + private void validateOwner(Order order, Long userId) { // 토큰 유저가 이 주문의 주인인지 if (order.getUser() == null || !order.getUser().getId().equals(userId)) { diff --git a/src/main/java/app/dearobjet/backend/domain/order/dto/OrderStatsResponse.java b/src/main/java/app/dearobjet/backend/domain/order/dto/OrderStatsResponse.java new file mode 100644 index 0000000..b1b443c --- /dev/null +++ b/src/main/java/app/dearobjet/backend/domain/order/dto/OrderStatsResponse.java @@ -0,0 +1,38 @@ +package app.dearobjet.backend.domain.order.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class OrderStatsResponse { + + private List sales; + private List popularItems; + private List ordersByHour; + + @Getter + @AllArgsConstructor + public static class SalesPoint { + private String date; + private long orderCount; + private long amount; + } + + @Getter + @AllArgsConstructor + public static class PopularItemPoint { + private Long itemId; + private long quantity; + private long salesAmount; + } + + @Getter + @AllArgsConstructor + public static class OrdersByHourPoint { + private int hour; + private long orderCount; + } +} \ No newline at end of file