diff --git a/src/main/java/com/goi/erp/controller/DriverDailyStatController.java b/src/main/java/com/goi/erp/controller/DriverDailyStatController.java new file mode 100644 index 0000000..a7fca64 --- /dev/null +++ b/src/main/java/com/goi/erp/controller/DriverDailyStatController.java @@ -0,0 +1,71 @@ +package com.goi.erp.controller; + +import com.goi.erp.common.permission.PermissionChecker; +import com.goi.erp.common.permission.PermissionSet; +import com.goi.erp.dto.DriverDailyStatHistoryResponseDto; +import com.goi.erp.dto.DriverDailyStatResponseDto; +import com.goi.erp.service.DriverDailyStatService; +import com.goi.erp.token.PermissionAuthenticationToken; + +import lombok.RequiredArgsConstructor; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; + +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/driver-daily-stat") +@RequiredArgsConstructor +public class DriverDailyStatController { + + private final DriverDailyStatService driverDailyStatService; + + private PermissionSet getPermission() { + PermissionAuthenticationToken auth = + (PermissionAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + if (auth == null || auth.getPermissionSet() == null) { + throw new AccessDeniedException("Permission information is missing"); + } + + return auth.getPermissionSet(); + } + + + // READ (대시보드: date default=오늘, driverId default=전체) + @GetMapping + public ResponseEntity> getDashboard( + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @RequestParam(required = false) Long driverId) { + + PermissionSet permissions = getPermission(); + if (!PermissionChecker.canReadCRMAll(permissions)) { + throw new AccessDeniedException("You do not have permission to read dispatch daily status"); + } + + return ResponseEntity.ok(driverDailyStatService.getDashboard(date, driverId)); + } + + // READ HISTORY (단일 드라이버 누적 이력: driverId 필수, date default=오늘) + @GetMapping("/history") + public ResponseEntity getDriverHistory( + @RequestParam Long driverId, + @RequestParam(required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + + PermissionSet permissions = getPermission(); + if (!PermissionChecker.canReadCRMAll(permissions)) { + throw new AccessDeniedException("You do not have permission to read dispatch history"); + } + + return ResponseEntity.ok(driverDailyStatService.getDriverHistory(date, driverId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryPointDto.java b/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryPointDto.java new file mode 100644 index 0000000..1cbc742 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryPointDto.java @@ -0,0 +1,22 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DriverDailyStatHistoryPointDto { + + private LocalDateTime t; // 픽업 시각 + private String customerName; // 고객명 (cus_name) + private BigDecimal qty; // 이 픽업의 양 + private BigDecimal cumQty; // 누적 픽업량 + private Long cumCompleted; // 누적 방문 완료 수 +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryResponseDto.java b/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryResponseDto.java new file mode 100644 index 0000000..32a7304 --- /dev/null +++ b/src/main/java/com/goi/erp/dto/DriverDailyStatHistoryResponseDto.java @@ -0,0 +1,20 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DriverDailyStatHistoryResponseDto { + + private Long driverId; // 드라이버 ID + private LocalDate date; // 조회 일자 + private List points; // 시각순 누적 포인트 (라인 1개) +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/dto/DriverDailyStatResponseDto.java b/src/main/java/com/goi/erp/dto/DriverDailyStatResponseDto.java new file mode 100644 index 0000000..f6ca78e --- /dev/null +++ b/src/main/java/com/goi/erp/dto/DriverDailyStatResponseDto.java @@ -0,0 +1,37 @@ +package com.goi.erp.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DriverDailyStatResponseDto { + + private Long ddsId; // 행 식별자 (프론트 row key용, vd.ved_id) + private String ddsStatus; // 배차 상태 (O 등) + private Integer ddsShift; // 교대(shift) + private LocalDate ddsDate; // 배차 일자 + private String ddsNumber; // 차량 번호 (예: 9호) + private Long ddsDriverId; // 드라이버 ID (필터/드롭다운용) + private String ddsMainDriver; // 주 드라이버 + private String ddsSubDriver; // 보조 드라이버 + private String ddsKm; // 이동거리 (예: "35.78 km") + private String ddsMin; // 운행시간 (예: "198.00 min") + private Integer ddsEvent; // 안전 이벤트 수 + private BigDecimal ddsTotalQty; // 실제 픽업량 합계 + private BigDecimal ddsPlannedQty; // 예상 픽업량 합계 (목표) + private Long ddsCompletedCount; // 방문 완료(F) 수 + private Long ddsPlannedCount; // 전체 방문 목표 수 + private LocalDateTime ddsStartAt; // 운행 시작 + private LocalDateTime ddsPausedAt; // 일시정지 + private LocalDateTime ddsEndAt; // 운행 종료 + private String ddsEndReason; // 종료 사유 +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/entity/DriverDailyStat.java b/src/main/java/com/goi/erp/entity/DriverDailyStat.java new file mode 100644 index 0000000..4898830 --- /dev/null +++ b/src/main/java/com/goi/erp/entity/DriverDailyStat.java @@ -0,0 +1,48 @@ +package com.goi.erp.entity; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.hibernate.annotations.Immutable; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Immutable // 읽기 전용 뷰 +@Table(schema = "rpt", name = "vw_driver_daily_stat") // 실제 생성한 스키마/뷰 이름에 맞춰 조정 +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DriverDailyStat { + + @Id + private Long ddsId; // vd.ved_id (뷰 PK용, SELECT에 dds_id 추가 필요) + private String ddsStatus; // 배차 상태 (O 등) + private Integer ddsShift; // 교대(shift) + private LocalDate ddsDate; // 배차 일자 + private String ddsNumber; // 차량 번호 (예: 9호) + private Long ddsDriverId; // 드라이버 ID (필터/드롭다운용) + private String ddsMainDriver; // 주 드라이버 + private String ddsSubDriver; // 보조 드라이버 + private String ddsKm; // 이동거리 (뷰에서 텍스트, 예: "35.78 km") + private String ddsMin; // 운행시간 (뷰에서 텍스트, 예: "198.00 min") + private Integer ddsEvent; // 안전 이벤트 수 + private BigDecimal ddsTotalQty; // 실제 픽업량 합계 + private BigDecimal ddsPlannedQty; // 예상 픽업량 합계 (목표) + private Long ddsCompletedCount; // 방문 완료(F) 수 + private Long ddsPlannedCount; // 전체 방문 목표 수 + private LocalDateTime ddsStartAt; // 운행 시작 + private LocalDateTime ddsPausedAt; // 일시정지 + private LocalDateTime ddsEndAt; // 운행 종료 + private String ddsEndReason; // 종료 사유 +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/DriverDailyStatRepository.java b/src/main/java/com/goi/erp/repository/DriverDailyStatRepository.java new file mode 100644 index 0000000..44e6dde --- /dev/null +++ b/src/main/java/com/goi/erp/repository/DriverDailyStatRepository.java @@ -0,0 +1,41 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.DriverDailyStat; + +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; + +@Repository +public interface DriverDailyStatRepository extends JpaRepository { + + // 날짜 기준 전체 드라이버 (운행 시작 시간 순) + List findByDdsDateOrderByDdsStartAtAsc(LocalDate ddsDate); + + // 날짜 + 특정 드라이버 (운행 시작 시간 순) + List findByDdsDateAndDdsDriverIdOrderByDdsStartAtAsc(LocalDate ddsDate, Long ddsDriverId); + + // [단일 드라이버] 픽업 이벤트 기반 누적 이력 (시각순) — 라인 차트용 + // - crm.customer_daily_order 의 cdo_pickup_at 으로 누적합 + // - return: [0]=픽업 시각, [1]=누적 픽업량, [2]=누적 방문완료 수 + @Query(value = """ + SELECT cdo.cdo_pickup_at AS t, + c.cus_name AS cus_name, + cdo.cdo_quantity AS qty, + SUM(cdo.cdo_quantity) OVER (ORDER BY cdo.cdo_pickup_at) AS cum_qty, + COUNT(*) FILTER (WHERE cast(cdo.cdo_status as text) = 'F') + OVER (ORDER BY cdo.cdo_pickup_at) AS cum_completed + FROM crm.customer_daily_order cdo + LEFT JOIN crm.customer c ON c.cus_id = cdo.cdo_customer_id + WHERE cdo.cdo_order_date = :date + AND cdo.cdo_driver_id = :driverId + AND cdo.cdo_pickup_at IS NOT NULL + ORDER BY cdo.cdo_pickup_at + """, nativeQuery = true) + List findDriverPickupHistory(@Param("date") LocalDate date, + @Param("driverId") Long driverId); +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/service/DriverDailyStatService.java b/src/main/java/com/goi/erp/service/DriverDailyStatService.java new file mode 100644 index 0000000..2719efa --- /dev/null +++ b/src/main/java/com/goi/erp/service/DriverDailyStatService.java @@ -0,0 +1,97 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.DriverDailyStatHistoryPointDto; +import com.goi.erp.dto.DriverDailyStatHistoryResponseDto; +import com.goi.erp.dto.DriverDailyStatResponseDto; +import com.goi.erp.entity.DriverDailyStat; +import com.goi.erp.repository.DriverDailyStatRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class DriverDailyStatService { + + private final DriverDailyStatRepository driverDailyStatRepository; + + /** + * 대시보드 조회 + * @param date 조회 일자 (null 이면 오늘) + * @param driverId 드라이버 ID (null 이면 전체) + */ + public List getDashboard(LocalDate date, Long driverId) { + + LocalDate targetDate = (date == null) ? LocalDate.now() : date; + + List rows = (driverId == null) + ? driverDailyStatRepository.findByDdsDateOrderByDdsStartAtAsc(targetDate) + : driverDailyStatRepository.findByDdsDateAndDdsDriverIdOrderByDdsStartAtAsc(targetDate, driverId); + + return rows.stream().map(this::mapToDto).toList(); + } + + /** + * 단일 드라이버 누적 이력 (이벤트 시각 기반, 라인 차트용) + * @param date 조회 일자 (null 이면 오늘) + * @param driverId 드라이버 ID (필수) + */ + public DriverDailyStatHistoryResponseDto getDriverHistory(LocalDate date, Long driverId) { + + LocalDate targetDate = (date == null) ? LocalDate.now() : date; + + List points = driverDailyStatRepository + .findDriverPickupHistory(targetDate, driverId) + .stream() + .map(r -> DriverDailyStatHistoryPointDto.builder() + .t(((Timestamp) r[0]).toLocalDateTime()) + .customerName((String) r[1]) + .qty(r[2] == null ? BigDecimal.ZERO : (BigDecimal) r[2]) + .cumQty(r[3] == null ? BigDecimal.ZERO : (BigDecimal) r[3]) + .cumCompleted(r[4] == null ? 0L : ((Number) r[4]).longValue()) + .build()) + .toList(); + + return DriverDailyStatHistoryResponseDto.builder() + .driverId(driverId) + .date(targetDate) + .points(points) + .build(); + } + + /** + * ENTITY → RESPONSE DTO + */ + public DriverDailyStatResponseDto mapToDto(DriverDailyStat s) { + + if (s == null) return null; + + return DriverDailyStatResponseDto.builder() + .ddsId(s.getDdsId()) + .ddsDriverId(s.getDdsDriverId()) + .ddsStatus(s.getDdsStatus()) + .ddsShift(s.getDdsShift()) + .ddsDate(s.getDdsDate()) + .ddsNumber(s.getDdsNumber()) + .ddsMainDriver(s.getDdsMainDriver()) + .ddsSubDriver(s.getDdsSubDriver()) + .ddsKm(s.getDdsKm()) + .ddsMin(s.getDdsMin()) + .ddsEvent(s.getDdsEvent()) + .ddsTotalQty(s.getDdsTotalQty()) + .ddsPlannedQty(s.getDdsPlannedQty()) + .ddsCompletedCount(s.getDdsCompletedCount()) + .ddsPlannedCount(s.getDdsPlannedCount()) + .ddsStartAt(s.getDdsStartAt()) + .ddsPausedAt(s.getDdsPausedAt()) + .ddsEndAt(s.getDdsEndAt()) + .ddsEndReason(s.getDdsEndReason()) + .build(); + } +} \ No newline at end of file