diff --git a/src/main/java/com/goi/erp/controller/CustomerController.java b/src/main/java/com/goi/erp/controller/CustomerController.java index e80df5e..7abebe4 100644 --- a/src/main/java/com/goi/erp/controller/CustomerController.java +++ b/src/main/java/com/goi/erp/controller/CustomerController.java @@ -5,6 +5,7 @@ import com.goi.erp.common.permission.PermissionSet; import com.goi.erp.dto.CustomerDailyOrderResponseDto; import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerResponseDto; +import com.goi.erp.dto.CustomerActiveTrendPointDto; import com.goi.erp.dto.CustomerSummaryResponseDto; import com.goi.erp.service.CustomerDailyOrderService; import com.goi.erp.service.CustomerService; @@ -22,6 +23,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; +import java.util.List; import java.util.UUID; @RestController @@ -104,6 +106,25 @@ public class CustomerController { return ResponseEntity.ok(customerService.getCustomerSummary()); } + // ACTIVE TREND (CS 홈 선그래프: 최근 N일 날짜별 활성 고객 수, 기본 7일) + @GetMapping("/active-trend") + public ResponseEntity> getActiveCustomerTrend( + @RequestParam(required = false, defaultValue = "7") int days) { + // 권한 체크 + PermissionAuthenticationToken auth = (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + PermissionSet permissionSet = auth.getPermissionSet(); + if (!PermissionChecker.canReadCRMAll(permissionSet)) { + throw new AccessDeniedException("You do not have permission to read all CRM data"); + } + + return ResponseEntity.ok(customerService.getActiveCustomerTrend(days)); + } + // READ ONE @GetMapping("/uuid/{uuid}") public ResponseEntity getCustomer(@PathVariable UUID uuid) { diff --git a/src/main/java/com/goi/erp/dto/CustomerActiveTrendPointDto.java b/src/main/java/com/goi/erp/dto/CustomerActiveTrendPointDto.java new file mode 100644 index 0000000..54e83e4 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerActiveTrendPointDto.java @@ -0,0 +1,24 @@ +package com.goi.erp.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomerActiveTrendPointDto { + + // 해당 날짜 (YYYY-MM-DD) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private LocalDate date; + + // 그 날짜에 활성( cus_contract_date <= date AND (terminated IS NULL OR terminated > date) )인 고객 수 + private long count; +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/CustomerRepository.java b/src/main/java/com/goi/erp/repository/CustomerRepository.java index 03b6d28..ba9b26d 100644 --- a/src/main/java/com/goi/erp/repository/CustomerRepository.java +++ b/src/main/java/com/goi/erp/repository/CustomerRepository.java @@ -6,9 +6,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.time.LocalDate; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -21,14 +23,33 @@ public interface CustomerRepository extends JpaRepository { Optional findByCusNo(String cusNo); boolean existsByCusNo(String cusNo); - - @Query(value = "select c from Customer c order by c.cusContractDate desc nulls last", - countQuery = "select count(c) from Customer c") - Page findAllOrderByContractDateDesc(Pageable pageable); // 현재(활성) 고객 수: cus_status = 'A' long countByCusStatus(String cusStatus); // 오늘 추가된 고객 수: cus_contract_date = 오늘 long countByCusContractDate(LocalDate cusContractDate); + + // 날짜별 활성 고객 수 (선그래프용). + // - 활성 기준 = cus_status='A' (현재 활성 플래그가 진짜 기준) + // - 그 위에 날짜로 과거 시점 재구성: cus_contract_date <= D AND (cus_terminated_date IS NULL OR cus_terminated_date > D) + // - 즉 "지금 활성('A')이면서 그 날짜에 계약기간 안에 있던" 고객 수 → 오늘 값은 전체(활성) 고객 수와 일치 + // - generate_series 로 [startDate, endDate] 각 날짜를 만들고 LEFT JOIN 으로 카운트 → 빈 날도 0으로 채워짐 + // - return: [0]=날짜(date), [1]=활성 고객 수(bigint) + @Query(value = """ + SELECT CAST(series.day AS date) AS day, + COUNT(c.cus_id) AS active_count + FROM generate_series(CAST(:startDate AS timestamp), + CAST(:endDate AS timestamp), + INTERVAL '1 day') AS series(day) + LEFT JOIN crm.customer c + ON c.cus_status = 'A' + AND c.cus_contract_date IS NOT NULL + AND c.cus_contract_date <= CAST(series.day AS date) + AND (c.cus_terminated_date IS NULL OR c.cus_terminated_date > CAST(series.day AS date)) + GROUP BY series.day + ORDER BY series.day + """, nativeQuery = true) + List findActiveCustomerTrend(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/service/CustomerService.java b/src/main/java/com/goi/erp/service/CustomerService.java index 5bd015d..585bb5b 100644 --- a/src/main/java/com/goi/erp/service/CustomerService.java +++ b/src/main/java/com/goi/erp/service/CustomerService.java @@ -2,6 +2,7 @@ package com.goi.erp.service; import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerResponseDto; +import com.goi.erp.dto.CustomerActiveTrendPointDto; import com.goi.erp.dto.CustomerSummaryResponseDto; import com.goi.erp.entity.Customer; import com.goi.erp.entity.EntityChangeLog; @@ -21,6 +22,8 @@ import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -84,7 +87,7 @@ public class CustomerService { public Page getAllCustomers(int page, int size) { Pageable pageable = PageRequest.of(page, size); - Page customers = customerRepository.findAllOrderByContractDateDesc(pageable); + Page customers = customerRepository.findAll(pageable); return customers.map(this::mapToDto); // 기존 mapToDto 사용 } @@ -102,6 +105,34 @@ public class CustomerService { .build(); } + // CS 홈 선그래프: 최근 N일(기본 7) 날짜별 활성 고객 수 + public List getActiveCustomerTrend(int days) { + int d = Math.min(Math.max(days, 1), 31); // 1~31일로 제한 + LocalDate end = LocalDate.now(); + LocalDate start = end.minusDays(d - 1L); // end 포함 N개 지점 + + List rows = customerRepository.findActiveCustomerTrend(start, end); + + List result = new ArrayList<>(rows.size()); + for (Object[] row : rows) { + LocalDate day; + if (row[0] instanceof java.sql.Date sqlDate) { + day = sqlDate.toLocalDate(); + } else if (row[0] instanceof LocalDate ld) { + day = ld; + } else { + day = LocalDate.parse(row[0].toString()); + } + long count = ((Number) row[1]).longValue(); + + result.add(CustomerActiveTrendPointDto.builder() + .date(day) + .count(count) + .build()); + } + return result; + } + public CustomerResponseDto getCustomerByUuid(UUID uuid) { Customer customer = customerRepository.findByCusUuid(uuid) .orElseThrow(() -> new RuntimeException("Customer not found"));