고객 수 추이 추가.

This commit is contained in:
Hyojin Ahn 2026-06-24 09:46:17 -04:00
parent 8736e0eb2e
commit 0b2e744a0f
4 changed files with 102 additions and 5 deletions

View File

@ -5,6 +5,7 @@ import com.goi.erp.common.permission.PermissionSet;
import com.goi.erp.dto.CustomerDailyOrderResponseDto; import com.goi.erp.dto.CustomerDailyOrderResponseDto;
import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerRequestDto;
import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.dto.CustomerActiveTrendPointDto;
import com.goi.erp.dto.CustomerSummaryResponseDto; import com.goi.erp.dto.CustomerSummaryResponseDto;
import com.goi.erp.service.CustomerDailyOrderService; import com.goi.erp.service.CustomerDailyOrderService;
import com.goi.erp.service.CustomerService; import com.goi.erp.service.CustomerService;
@ -22,6 +23,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@RestController @RestController
@ -104,6 +106,25 @@ public class CustomerController {
return ResponseEntity.ok(customerService.getCustomerSummary()); return ResponseEntity.ok(customerService.getCustomerSummary());
} }
// ACTIVE TREND (CS 선그래프: 최근 N일 날짜별 활성 고객 , 기본 7일)
@GetMapping("/active-trend")
public ResponseEntity<List<CustomerActiveTrendPointDto>> 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 // READ ONE
@GetMapping("/uuid/{uuid}") @GetMapping("/uuid/{uuid}")
public ResponseEntity<CustomerResponseDto> getCustomer(@PathVariable UUID uuid) { public ResponseEntity<CustomerResponseDto> getCustomer(@PathVariable UUID uuid) {

View File

@ -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;
}

View File

@ -6,9 +6,11 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -22,13 +24,32 @@ public interface CustomerRepository extends JpaRepository<Customer, Long> {
boolean existsByCusNo(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<Customer> findAllOrderByContractDateDesc(Pageable pageable);
// 현재(활성) 고객 : cus_status = 'A' // 현재(활성) 고객 : cus_status = 'A'
long countByCusStatus(String cusStatus); long countByCusStatus(String cusStatus);
// 오늘 추가된 고객 : cus_contract_date = 오늘 // 오늘 추가된 고객 : cus_contract_date = 오늘
long countByCusContractDate(LocalDate cusContractDate); 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<Object[]> findActiveCustomerTrend(@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
} }

View File

@ -2,6 +2,7 @@ package com.goi.erp.service;
import com.goi.erp.dto.CustomerRequestDto; import com.goi.erp.dto.CustomerRequestDto;
import com.goi.erp.dto.CustomerResponseDto; import com.goi.erp.dto.CustomerResponseDto;
import com.goi.erp.dto.CustomerActiveTrendPointDto;
import com.goi.erp.dto.CustomerSummaryResponseDto; import com.goi.erp.dto.CustomerSummaryResponseDto;
import com.goi.erp.entity.Customer; import com.goi.erp.entity.Customer;
import com.goi.erp.entity.EntityChangeLog; import com.goi.erp.entity.EntityChangeLog;
@ -21,6 +22,8 @@ import java.lang.reflect.Field;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -84,7 +87,7 @@ public class CustomerService {
public Page<CustomerResponseDto> getAllCustomers(int page, int size) { public Page<CustomerResponseDto> getAllCustomers(int page, int size) {
Pageable pageable = PageRequest.of(page, size); Pageable pageable = PageRequest.of(page, size);
Page<Customer> customers = customerRepository.findAllOrderByContractDateDesc(pageable); Page<Customer> customers = customerRepository.findAll(pageable);
return customers.map(this::mapToDto); // 기존 mapToDto 사용 return customers.map(this::mapToDto); // 기존 mapToDto 사용
} }
@ -102,6 +105,34 @@ public class CustomerService {
.build(); .build();
} }
// CS 선그래프: 최근 N일(기본 7) 날짜별 활성 고객
public List<CustomerActiveTrendPointDto> 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<Object[]> rows = customerRepository.findActiveCustomerTrend(start, end);
List<CustomerActiveTrendPointDto> 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) { public CustomerResponseDto getCustomerByUuid(UUID uuid) {
Customer customer = customerRepository.findByCusUuid(uuid) Customer customer = customerRepository.findByCusUuid(uuid)
.orElseThrow(() -> new RuntimeException("Customer not found")); .orElseThrow(() -> new RuntimeException("Customer not found"));