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 @@ -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")
Expand Down Expand Up @@ -79,4 +81,13 @@ public ApiResponse<Void> confirmPurchase(
orderService.confirmPurchase(userDetails.getUserId(), orderId);
return ApiResponse.of(null);
}

@GetMapping("/orders/stats")
public ApiResponse<OrderStatsResponse> getOrderStats(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam LocalDate from,
@RequestParam LocalDate to
) {
return ApiResponse.of(orderService.getOrderStats(userDetails.getUserId(), from, to));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Order, Long> {

Page<Order> findByUser(User user, Pageable pageable);
Page<Order> 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<Object[]> 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<Object[]> 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<Object[]> findOrdersByHour(
@Param("userId") Long userId,
@Param("fromDt") LocalDateTime fromDt,
@Param("toDt") LocalDateTime toDt
);
}
40 changes: 40 additions & 0 deletions src/main/java/app/dearobjet/backend/domain/order/OrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Object[]> salesRows = orderRepository.findSalesDaily(userId, fromDt, toDt);
List<OrderStatsResponse.SalesPoint> 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<Object[]> popularRows = orderRepository.findPopularItemsTop10(userId, fromDt, toDt);
List<OrderStatsResponse.PopularItemPoint> 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<Object[]> hourRows = orderRepository.findOrdersByHour(userId, fromDt, toDt);
List<OrderStatsResponse.OrdersByHourPoint> 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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SalesPoint> sales;
private List<PopularItemPoint> popularItems;
private List<OrdersByHourPoint> 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;
}
}