From dcfd7e9eaab8f0a7f7f7a6af87d95cfdd32e46de Mon Sep 17 00:00:00 2001 From: lsh9672 Date: Tue, 17 Jun 2025 09:25:22 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=A3=BC=EB=AC=B8=ED=86=B5=EA=B3=84?= =?UTF-8?q?=20url=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infra/dto/OrderListResponse.java | 19 +++ .../hi/analytics/infra/dto/OrderResponse.java | 24 +++ .../infra/gateway/OrderDataAdapter.java | 159 +++++++++++++++++- 3 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderListResponse.java create mode 100644 analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderResponse.java diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderListResponse.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderListResponse.java new file mode 100644 index 0000000..7e13bed --- /dev/null +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderListResponse.java @@ -0,0 +1,19 @@ +package com.ktds.hi.analytics.infra.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderListResponse { + private Long storeId; + private Integer totalCount; + private List orders; +} + diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderResponse.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderResponse.java new file mode 100644 index 0000000..8467107 --- /dev/null +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/dto/OrderResponse.java @@ -0,0 +1,24 @@ +package com.ktds.hi.analytics.infra.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderResponse { + private Long id; + private Long storeId; + private Long menuId; + private Integer customerAge; + private String customerGender; + private BigDecimal orderAmount; + private LocalDateTime orderDate; + private LocalDateTime createdAt; +} diff --git a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/OrderDataAdapter.java b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/OrderDataAdapter.java index 538310e..aeb4c47 100644 --- a/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/OrderDataAdapter.java +++ b/analytics/src/main/java/com/ktds/hi/analytics/infra/gateway/OrderDataAdapter.java @@ -2,16 +2,27 @@ package com.ktds.hi.analytics.infra.gateway; import com.ktds.hi.analytics.biz.domain.OrderStatistics; import com.ktds.hi.analytics.biz.usecase.out.OrderDataPort; +import com.ktds.hi.analytics.infra.dto.OrderListResponse; +import com.ktds.hi.analytics.infra.dto.OrderResponse; +import com.ktds.hi.common.dto.SuccessResponse; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * 주문 데이터 어댑터 클래스 @@ -32,15 +43,32 @@ public class OrderDataAdapter implements OrderDataPort { log.info("주문 통계 조회: storeId={}, period={} ~ {}", storeId, startDate, endDate); try { + + // LocalDate를 LocalDateTime으로 변환 (시작일은 00:00:00, 종료일은 23:59:59) + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(23, 59, 59); + String url = String.format("%s/api/stores/orders/store/%d/period?startDate=%s&endDate=%s", - storeServiceUrl, storeId, startDate, endDate); + storeServiceUrl, storeId, startDateTime, endDateTime); - OrderStatistics statistics = restTemplate.getForObject(url, OrderStatistics.class); - - if (statistics != null) { - log.info("주문 통계 조회 완료: storeId={}, totalOrders={}", - storeId, statistics.getTotalOrders()); + // ParameterizedTypeReference를 사용하여 제네릭 타입 안전하게 파싱 + ParameterizedTypeReference> responseType = + new ParameterizedTypeReference>() {}; + + ResponseEntity> responseEntity = + restTemplate.exchange(url, HttpMethod.GET, null, responseType); + + SuccessResponse successResponse = responseEntity.getBody(); + + if (successResponse != null && successResponse.isSuccess() && successResponse.getData() != null) { + OrderListResponse orderListResponse = successResponse.getData(); + + // OrderListResponse를 OrderStatistics로 변환 + OrderStatistics statistics = convertToOrderStatistics(orderListResponse); + + log.info("주문 통계 조회 완료: storeId={}, totalOrders={}", + storeId, statistics.getTotalOrders()); return statistics; } @@ -51,6 +79,125 @@ public class OrderDataAdapter implements OrderDataPort { // 실패 시 더미 데이터 반환 return createDummyOrderStatistics(storeId); } + + /** + * OrderListResponse를 OrderStatistics로 변환 + */ + private OrderStatistics convertToOrderStatistics(OrderListResponse orderListResponse) { + List orders = orderListResponse.getOrders(); + + if (orders == null || orders.isEmpty()) { + return createEmptyOrderStatistics(); + } + + // 총 주문 수 + int totalOrders = orders.size(); + + // 총 매출 계산 + BigDecimal totalRevenue = orders.stream() + .map(OrderResponse::getOrderAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + // 평균 주문 금액 계산 + double averageOrderValue = totalRevenue.doubleValue() / totalOrders; + + // 시간대별 주문 분석 (피크 시간 계산) + Map hourlyOrderCount = orders.stream() + .collect(Collectors.groupingBy( + order -> order.getOrderDate().getHour(), + Collectors.counting() + )); + + int peakHour = hourlyOrderCount.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse(12); // 기본값 12시 + + // 연령대별 분포 계산 + Map ageDistribution = calculateCustomerAgeDistribution(orders); + + // 인기 메뉴 계산 (메뉴ID별 주문 횟수) - 실제로는 메뉴명을 가져와야 하지만 임시로 메뉴ID 사용 + List popularMenus = orders.stream() + .collect(Collectors.groupingBy( + OrderResponse::getMenuId, + Collectors.counting() + )) + .entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(4) + .map(entry -> "메뉴" + entry.getKey()) // 실제로는 메뉴명으로 변환 필요 + .collect(Collectors.toList()); + + return OrderStatistics.builder() + .totalOrders(totalOrders) + .totalRevenue(totalRevenue.longValue()) + .averageOrderValue(averageOrderValue) + .peakHour(peakHour) + .popularMenus(popularMenus) + .customerAgeDistribution(ageDistribution) + .build(); + } + + /** + * 연령대별 분포 계산 + */ + private Map calculateCustomerAgeDistribution(List orders) { + Map ageGroupCount = orders.stream() + .collect(Collectors.groupingBy( + order -> getAgeGroup(order.getCustomerAge()), + Collectors.counting() + )); + + // Long을 Integer로 변환 + Map result = new HashMap<>(); + ageGroupCount.forEach((key, value) -> result.put(key, value.intValue())); + + return result; + } + + /** + * 연령대별 분포 계산 + */ + private Map calculateAgeDistribution(List orders) { + Map ageGroupCount = orders.stream() + .collect(Collectors.groupingBy( + order -> getAgeGroup(order.getCustomerAge()), + Collectors.counting() + )); + + // Long을 Integer로 변환 + Map result = new HashMap<>(); + ageGroupCount.forEach((key, value) -> result.put(key, value.intValue())); + + return result; + } + + /** + * 나이를 연령대 그룹으로 변환 + */ + private String getAgeGroup(Integer age) { + if (age == null) return "미분류"; + if (age < 20) return "10대"; + if (age < 30) return "20대"; + if (age < 40) return "30대"; + if (age < 50) return "40대"; + if (age < 60) return "50대"; + return "60대+"; + } + + /** + * 빈 주문 통계 생성 + */ + private OrderStatistics createEmptyOrderStatistics() { + return OrderStatistics.builder() + .totalOrders(0) + .totalRevenue(0L) + .averageOrderValue(0.0) + .peakHour(12) + .popularMenus(Arrays.asList()) + .customerAgeDistribution(new HashMap<>()) + .build(); + } @Override public Integer getCurrentOrderCount(Long storeId) {