Skip to content

Commit 7c40eaa

Browse files
Merge pull request #29 from DearObjet/feature/order-stats
[FEAT] 주문 통계(일별 매출/인기상품 TOP10/시간대별 주문수) 기능 구현
2 parents d07aee1 + 34d3a72 commit 7c40eaa

File tree

4 files changed

+154
-1
lines changed

4 files changed

+154
-1
lines changed

src/main/java/app/dearobjet/backend/domain/order/OrderController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.springframework.security.core.annotation.AuthenticationPrincipal;
99
import org.springframework.web.bind.annotation.*;
1010

11+
import java.time.LocalDate;
12+
1113
@RestController
1214
@RequiredArgsConstructor
1315
@RequestMapping("/api/v1")
@@ -79,4 +81,13 @@ public ApiResponse<Void> confirmPurchase(
7981
orderService.confirmPurchase(userDetails.getUserId(), orderId);
8082
return ApiResponse.of(null);
8183
}
84+
85+
@GetMapping("/orders/stats")
86+
public ApiResponse<OrderStatsResponse> getOrderStats(
87+
@AuthenticationPrincipal CustomUserDetails userDetails,
88+
@RequestParam LocalDate from,
89+
@RequestParam LocalDate to
90+
) {
91+
return ApiResponse.of(orderService.getOrderStats(userDetails.getUserId(), from, to));
92+
}
8293
}

src/main/java/app/dearobjet/backend/domain/order/OrderRepository.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,72 @@
55
import app.dearobjet.backend.domain.user.entity.User;
66
import org.springframework.data.domain.*;
77
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Query;
9+
import org.springframework.data.repository.query.Param;
10+
11+
import java.time.LocalDateTime;
12+
import java.util.List;
813

914
public interface OrderRepository extends JpaRepository<Order, Long> {
15+
1016
Page<Order> findByUser(User user, Pageable pageable);
1117
Page<Order> findByUserAndStatus(User user, OrderStatus status, Pageable pageable);
12-
}
18+
19+
// 1) 일별 매출
20+
@Query(value = """
21+
SELECT DATE(o.paid_at) AS d,
22+
COUNT(*) AS cnt,
23+
COALESCE(SUM(o.total_amount), 0) AS amt
24+
FROM orders o
25+
WHERE o.user_id = :userId
26+
AND o.paid_at IS NOT NULL
27+
AND o.paid_at BETWEEN :fromDt AND :toDt
28+
AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED')
29+
GROUP BY DATE(o.paid_at)
30+
ORDER BY d ASC
31+
""", nativeQuery = true)
32+
List<Object[]> findSalesDaily(
33+
@Param("userId") Long userId,
34+
@Param("fromDt") LocalDateTime fromDt,
35+
@Param("toDt") LocalDateTime toDt
36+
);
37+
38+
// 2) 인기 상품 Top 10
39+
@Query(value = """
40+
SELECT oi.item_id AS itemId,
41+
COALESCE(SUM(oi.quantity), 0) AS qty,
42+
COALESCE(SUM(oi.line_amount), 0) AS sales
43+
FROM orders o
44+
JOIN order_item oi ON oi.orders_id = o.orders_id
45+
WHERE o.user_id = :userId
46+
AND o.paid_at IS NOT NULL
47+
AND o.paid_at BETWEEN :fromDt AND :toDt
48+
AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED')
49+
GROUP BY oi.item_id
50+
ORDER BY sales DESC
51+
LIMIT 10
52+
""", nativeQuery = true)
53+
List<Object[]> findPopularItemsTop10(
54+
@Param("userId") Long userId,
55+
@Param("fromDt") LocalDateTime fromDt,
56+
@Param("toDt") LocalDateTime toDt
57+
);
58+
59+
// 3) 시간대별 주문수
60+
@Query(value = """
61+
SELECT CAST(EXTRACT(HOUR FROM o.paid_at) AS INT) AS h,
62+
COUNT(*) AS cnt
63+
FROM orders o
64+
WHERE o.user_id = :userId
65+
AND o.paid_at IS NOT NULL
66+
AND o.paid_at BETWEEN :fromDt AND :toDt
67+
AND o.status IN ('PAID', 'SHIPPING', 'DELIVERED', 'COMPLETED')
68+
GROUP BY CAST(EXTRACT(HOUR FROM o.paid_at) AS INT)
69+
ORDER BY h ASC
70+
""", nativeQuery = true)
71+
List<Object[]> findOrdersByHour(
72+
@Param("userId") Long userId,
73+
@Param("fromDt") LocalDateTime fromDt,
74+
@Param("toDt") LocalDateTime toDt
75+
);
76+
}

src/main/java/app/dearobjet/backend/domain/order/OrderService.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import org.springframework.transaction.annotation.Transactional;
1313

1414
import java.time.LocalDate;
15+
import java.time.LocalDateTime;
16+
import java.time.LocalTime;
1517
import java.time.format.DateTimeFormatter;
1618
import java.util.ArrayList;
1719
import java.util.List;
@@ -131,6 +133,44 @@ public void confirmPurchase(Long userId, Long orderId) {
131133
order.updateStatus(OrderStatus.COMPLETED);
132134
}
133135

136+
@Transactional(readOnly = true)
137+
public OrderStatsResponse getOrderStats(Long userId, LocalDate from, LocalDate to) {
138+
139+
LocalDateTime fromDt = from.atStartOfDay();
140+
LocalDateTime toDt = LocalDateTime.of(to, LocalTime.of(23, 59, 59));
141+
142+
// 1) 일별 매출
143+
List<Object[]> salesRows = orderRepository.findSalesDaily(userId, fromDt, toDt);
144+
List<OrderStatsResponse.SalesPoint> sales = new ArrayList<>();
145+
for (Object[] row : salesRows) {
146+
String date = String.valueOf(row[0]);
147+
long cnt = ((Number) row[1]).longValue();
148+
long amt = ((Number) row[2]).longValue();
149+
sales.add(new OrderStatsResponse.SalesPoint(date, cnt, amt));
150+
}
151+
152+
// 2) 인기 상품 Top 10
153+
List<Object[]> popularRows = orderRepository.findPopularItemsTop10(userId, fromDt, toDt);
154+
List<OrderStatsResponse.PopularItemPoint> popularItems = new ArrayList<>();
155+
for (Object[] row : popularRows) {
156+
Long itemId = ((Number) row[0]).longValue();
157+
long qty = ((Number) row[1]).longValue();
158+
long salesAmt = ((Number) row[2]).longValue();
159+
popularItems.add(new OrderStatsResponse.PopularItemPoint(itemId, qty, salesAmt));
160+
}
161+
162+
// 3) 시간대별 주문수
163+
List<Object[]> hourRows = orderRepository.findOrdersByHour(userId, fromDt, toDt);
164+
List<OrderStatsResponse.OrdersByHourPoint> ordersByHour = new ArrayList<>();
165+
for (Object[] row : hourRows) {
166+
int hour = ((Number) row[0]).intValue();
167+
long cnt = ((Number) row[1]).longValue();
168+
ordersByHour.add(new OrderStatsResponse.OrdersByHourPoint(hour, cnt));
169+
}
170+
171+
return new OrderStatsResponse(sales, popularItems, ordersByHour);
172+
}
173+
134174
private void validateOwner(Order order, Long userId) {
135175
// 토큰 유저가 이 주문의 주인인지
136176
if (order.getUser() == null || !order.getUser().getId().equals(userId)) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package app.dearobjet.backend.domain.order.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
import java.util.List;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public class OrderStatsResponse {
11+
12+
private List<SalesPoint> sales;
13+
private List<PopularItemPoint> popularItems;
14+
private List<OrdersByHourPoint> ordersByHour;
15+
16+
@Getter
17+
@AllArgsConstructor
18+
public static class SalesPoint {
19+
private String date;
20+
private long orderCount;
21+
private long amount;
22+
}
23+
24+
@Getter
25+
@AllArgsConstructor
26+
public static class PopularItemPoint {
27+
private Long itemId;
28+
private long quantity;
29+
private long salesAmount;
30+
}
31+
32+
@Getter
33+
@AllArgsConstructor
34+
public static class OrdersByHourPoint {
35+
private int hour;
36+
private long orderCount;
37+
}
38+
}

0 commit comments

Comments
 (0)