From e7faffa262232f8ff047e69fdcdda107ec81dc9b Mon Sep 17 00:00:00 2001 From: Hyojin Ahn Date: Thu, 18 Jun 2026 16:24:19 -0400 Subject: [PATCH] =?UTF-8?q?=EB=AA=A9=ED=91=9C=EB=9F=89=20=EC=B7=A8?= =?UTF-8?q?=ED=95=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=9B=B9=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CustomerDailyUcoController.java | 51 +++++++ .../erp/dto/CustomerDailyUcoRequestDto.java | 40 ++++++ .../erp/dto/CustomerDailyUcoResponseDto.java | 34 +++++ .../erp/dto/DriverDailyStatResponseDto.java | 6 + .../com/goi/erp/entity/CustomerDailyUco.java | 56 ++++++++ .../com/goi/erp/entity/DriverDailyStat.java | 8 +- .../CustomerDailyUcoRepository.java | 51 +++++++ .../erp/service/CustomerDailyUcoService.java | 133 ++++++++++++++++++ .../erp/service/DriverDailyStatService.java | 5 + 9 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/goi/erp/controller/CustomerDailyUcoController.java create mode 100644 src/main/java/com/goi/erp/dto/CustomerDailyUcoRequestDto.java create mode 100644 src/main/java/com/goi/erp/dto/CustomerDailyUcoResponseDto.java create mode 100644 src/main/java/com/goi/erp/entity/CustomerDailyUco.java create mode 100644 src/main/java/com/goi/erp/repository/CustomerDailyUcoRepository.java create mode 100644 src/main/java/com/goi/erp/service/CustomerDailyUcoService.java diff --git a/src/main/java/com/goi/erp/controller/CustomerDailyUcoController.java b/src/main/java/com/goi/erp/controller/CustomerDailyUcoController.java new file mode 100644 index 0000000..d03c3d0 --- /dev/null +++ b/src/main/java/com/goi/erp/controller/CustomerDailyUcoController.java @@ -0,0 +1,51 @@ +package com.goi.erp.controller; + +import com.goi.erp.dto.CustomerDailyUcoRequestDto; +import com.goi.erp.dto.CustomerDailyUcoResponseDto; +import com.goi.erp.service.CustomerDailyUcoService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/customer-daily-uco") +@RequiredArgsConstructor +public class CustomerDailyUcoController { + + private final CustomerDailyUcoService customerDailyUcoService; + + /** 대시보드: 특정 일자 전체 드라이버 목표/실적 */ + @GetMapping + public ResponseEntity> getByDate( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + return ResponseEntity.ok(customerDailyUcoService.getByDate(date)); + } + + /** 단일 드라이버 + 일자 */ + @GetMapping("/driver") + public ResponseEntity getByDriverAndDate( + @RequestParam Long driverId, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + CustomerDailyUcoResponseDto dto = customerDailyUcoService.getByDriverAndDate(driverId, date); + return dto != null ? ResponseEntity.ok(dto) : ResponseEntity.notFound().build(); + } + + /** MIS 08:00 목표 동기화 (upsert) — 스케줄러 pull 또는 MIS push 둘 다 사용 */ + @PostMapping("/target") + public ResponseEntity syncTarget(@RequestBody CustomerDailyUcoRequestDto dto) { + customerDailyUcoService.syncTarget(dto); + return ResponseEntity.ok().build(); + } + + /** 드라이버 언로딩 입력 (실측 + 사진) */ + @PutMapping("/unload") + public ResponseEntity recordUnload(@RequestBody CustomerDailyUcoRequestDto dto) { + return ResponseEntity.ok(customerDailyUcoService.recordUnload(dto)); + } +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/dto/CustomerDailyUcoRequestDto.java b/src/main/java/com/goi/erp/dto/CustomerDailyUcoRequestDto.java new file mode 100644 index 0000000..a00a0ef --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerDailyUcoRequestDto.java @@ -0,0 +1,40 @@ +package com.goi.erp.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@NoArgsConstructor +public class CustomerDailyUcoRequestDto { + + private LocalDate cduDate; // 대상 날짜 (YYYY-MM-DD) + + // 드라이버 식별 (MIS 는 외부 driverId, ERP 는 uuid 로 호출) + private String cduExternalDriverId; // MIS driverId → employee_external_map 참조 + private Long cduDriverId; // 배정 driver id + private UUID cduDriverUuid; // ERP 에서는 uuid 로 호출 + + // 목표 (MIS 08:00 동기화) + private BigDecimal cduTargetQty; // 목표량 + private Integer cduTargetVisitCnt; // 목표 방문수 + private LocalDateTime cduTargetSyncedAt; // 동기화 시각 (서버 set 가능) + + // 실측 (드라이버 포탈/MIS 언로딩 입력) + private BigDecimal cduActualQty; // 실제 언로딩량 + private BigDecimal cduActualWeight; // 무게 변환값 + private LocalDateTime cduUnloadedAt; // 언로딩 입력 시각 + private String cduUnloadPhoto; // 언로딩 사진 경로 + private String cduNote; // 메모 + + // 감사 컬럼 (MIS memberId → employee_external_map) + private String cduExternalCreatedBy; // MIS memberId → employee_external_map 참조 + private String cduExternalUpdatedBy; // MIS memberId → employee_external_map 참조 + private String cduCreatedBy; // 생성자 ID + private String cduUpdatedBy; // 수정자 ID + private String cduLoginUser; // 로그인 User (CreatedBy/UpdatedBy 용) +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/dto/CustomerDailyUcoResponseDto.java b/src/main/java/com/goi/erp/dto/CustomerDailyUcoResponseDto.java new file mode 100644 index 0000000..77b17af --- /dev/null +++ b/src/main/java/com/goi/erp/dto/CustomerDailyUcoResponseDto.java @@ -0,0 +1,34 @@ +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; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomerDailyUcoResponseDto { + + private UUID cduUuid; // UUID + private LocalDate cduDate; // 대상 날짜 (YYYY-MM-DD) + private Long cduDriverId; // Driver ID + private BigDecimal cduTargetQty; // 목표량 + private Integer cduTargetVisitCnt; // 목표 방문수 + private LocalDateTime cduTargetSyncedAt; // MIS 목표량 동기화 시각 + private BigDecimal cduActualQty; // 실제 언로딩량 + private BigDecimal cduActualWeight; // 무게 변환값 + private LocalDateTime cduUnloadedAt; // 언로딩 입력 시각 + private String cduUnloadPhoto; // 언로딩 사진 경로 + private String cduNote; // 메모 + private LocalDateTime cduCreatedAt; // 생성 시간 + private String cduCreatedBy; // 생성자 + private LocalDateTime cduUpdatedAt; // 수정 시간 + private String cduUpdatedBy; // 수정자 +} \ 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 index f6ca78e..c2acf04 100644 --- a/src/main/java/com/goi/erp/dto/DriverDailyStatResponseDto.java +++ b/src/main/java/com/goi/erp/dto/DriverDailyStatResponseDto.java @@ -34,4 +34,10 @@ public class DriverDailyStatResponseDto { private LocalDateTime ddsPausedAt; // 일시정지 private LocalDateTime ddsEndAt; // 운행 종료 private String ddsEndReason; // 종료 사유 + + private BigDecimal ddsTargetQty; // 목표량 (8시 고정, % 분모) + private Integer ddsTargetCount; // 목표 방문수 (8시 고정) + private BigDecimal ddsActualQty; // 실제 언로딩량 (NULL = 미입력) + private BigDecimal ddsActualWeight; // 무게 변환값 (NULL = 미입력) + private LocalDateTime ddsUnloadedAt; // 언로딩 입력 시각 (NULL = 미입력) } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/entity/CustomerDailyUco.java b/src/main/java/com/goi/erp/entity/CustomerDailyUco.java new file mode 100644 index 0000000..b4fc105 --- /dev/null +++ b/src/main/java/com/goi/erp/entity/CustomerDailyUco.java @@ -0,0 +1,56 @@ +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 java.util.UUID; + +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(schema = "crm", name = "customer_daily_uco") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class CustomerDailyUco { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long cduId; + private UUID cduUuid; + private LocalDate cduDate; + private Long cduDriverId; + private BigDecimal cduTargetQty; // 목표량 + private Integer cduTargetVisitCnt; // 목표 방문수 + private LocalDateTime cduTargetSyncedAt; // MIS 목표량 동기화 시각 + private BigDecimal cduActualQty; // 실제 언로딩량 + private BigDecimal cduActualWeight; // 무게로 변환 + private LocalDateTime cduUnloadedAt; // 언로딩 입력 시각 + + @Column(length = 255) + private String cduUnloadPhoto; + private String cduNote; + private LocalDateTime cduCreatedAt; + + @Column(name = "cdu_created_by") + private String cduCreatedBy; + private LocalDateTime cduUpdatedAt; + + @Column(name = "cdu_updated_by") + private String cduUpdatedBy; +} \ 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 index 4898830..b240973 100644 --- a/src/main/java/com/goi/erp/entity/DriverDailyStat.java +++ b/src/main/java/com/goi/erp/entity/DriverDailyStat.java @@ -38,11 +38,17 @@ public class DriverDailyStat { private String ddsMin; // 운행시간 (뷰에서 텍스트, 예: "198.00 min") private Integer ddsEvent; // 안전 이벤트 수 private BigDecimal ddsTotalQty; // 실제 픽업량 합계 - private BigDecimal ddsPlannedQty; // 예상 픽업량 합계 (목표) + private BigDecimal ddsPlannedQty; // 예상 픽업량 합계 private Long ddsCompletedCount; // 방문 완료(F) 수 private Long ddsPlannedCount; // 전체 방문 목표 수 private LocalDateTime ddsStartAt; // 운행 시작 private LocalDateTime ddsPausedAt; // 일시정지 private LocalDateTime ddsEndAt; // 운행 종료 private String ddsEndReason; // 종료 사유 + + private BigDecimal ddsTargetQty; // 목표량 (8시 MIS 스냅샷, 고정 분모) + private Integer ddsTargetCount; // 목표 방문수 (8시 고정) + private BigDecimal ddsActualQty; // 실제 언로딩량 (NULL = 미입력) + private BigDecimal ddsActualWeight; // 무게 변환값 (NULL = 미입력) + private LocalDateTime ddsUnloadedAt; // 언로딩 입력 시각 (NULL = 미입력) } \ No newline at end of file diff --git a/src/main/java/com/goi/erp/repository/CustomerDailyUcoRepository.java b/src/main/java/com/goi/erp/repository/CustomerDailyUcoRepository.java new file mode 100644 index 0000000..fe56241 --- /dev/null +++ b/src/main/java/com/goi/erp/repository/CustomerDailyUcoRepository.java @@ -0,0 +1,51 @@ +package com.goi.erp.repository; + +import com.goi.erp.entity.CustomerDailyUco; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface CustomerDailyUcoRepository extends JpaRepository { + + // ── 조회 (대시보드 / 드릴다운) ── + List findByCduDate(LocalDate cduDate); + + Optional findByCduDriverIdAndCduDate(Long cduDriverId, LocalDate cduDate); + + List findByCduDriverIdAndCduDateBetween( + Long cduDriverId, LocalDate from, LocalDate to); + + // ── MIS 08:00 목표 upsert (목표 컬럼만 갱신, 실측값 보존) ── + @Modifying + @Query(value = """ + INSERT INTO crm.customer_daily_uco + (cdu_date, cdu_driver_id, + cdu_target_qty, cdu_target_visit_cnt, cdu_target_synced_at, + cdu_created_by, cdu_updated_by) + VALUES + (:date, :driverId, + :targetQty, :targetVisitCnt, :syncedAt, + :loginUser, :loginUser) + ON CONFLICT (cdu_driver_id, cdu_date) + DO UPDATE SET + cdu_target_qty = EXCLUDED.cdu_target_qty, + cdu_target_visit_cnt = EXCLUDED.cdu_target_visit_cnt, + cdu_target_synced_at = EXCLUDED.cdu_target_synced_at, + cdu_updated_by = EXCLUDED.cdu_updated_by, + cdu_updated_at = CURRENT_TIMESTAMP + """, nativeQuery = true) + int upsertTarget(@Param("date") LocalDate date, + @Param("driverId") Long driverId, + @Param("targetQty") BigDecimal targetQty, + @Param("targetVisitCnt") Integer targetVisitCnt, + @Param("syncedAt") LocalDateTime syncedAt, + @Param("loginUser") String loginUser); +} \ No newline at end of file diff --git a/src/main/java/com/goi/erp/service/CustomerDailyUcoService.java b/src/main/java/com/goi/erp/service/CustomerDailyUcoService.java new file mode 100644 index 0000000..af98a60 --- /dev/null +++ b/src/main/java/com/goi/erp/service/CustomerDailyUcoService.java @@ -0,0 +1,133 @@ +package com.goi.erp.service; + +import com.goi.erp.dto.CustomerDailyUcoRequestDto; +import com.goi.erp.dto.CustomerDailyUcoResponseDto; +import com.goi.erp.entity.CustomerDailyUco; +import com.goi.erp.repository.CustomerDailyUcoRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomerDailyUcoService { + + private final CustomerDailyUcoRepository customerDailyUcoRepository; + private final HcmEmployeeClient hcmEmployeeClient; + + @Transactional(readOnly = true) + public List getByDate(LocalDate date) { + return customerDailyUcoRepository.findByCduDate(date).stream() + .map(this::toResponseDto) + .toList(); + } + + @Transactional(readOnly = true) + public CustomerDailyUcoResponseDto getByDriverAndDate(Long driverId, LocalDate date) { + return customerDailyUcoRepository.findByCduDriverIdAndCduDate(driverId, date) + .map(this::toResponseDto) + .orElse(null); + } + + /** MIS 08:00 목표 동기화 — 목표 컬럼만 upsert, 실측값은 보존 */ + @Transactional + public void syncTarget(CustomerDailyUcoRequestDto dto) { + Long driverId = resolveDriverId(dto); + LocalDateTime syncedAt = dto.getCduTargetSyncedAt() != null + ? dto.getCduTargetSyncedAt() : LocalDateTime.now(); + + customerDailyUcoRepository.upsertTarget( + dto.getCduDate(), driverId, + dto.getCduTargetQty(), dto.getCduTargetVisitCnt(), syncedAt, + dto.getCduLoginUser()); + } + + /** 드라이버 언로딩 입력 — 기존(목표) 행에 실측/사진 갱신, 없으면 신규 생성 */ + @Transactional + public CustomerDailyUcoResponseDto recordUnload(CustomerDailyUcoRequestDto dto) { + Long driverId = resolveDriverId(dto); + LocalDateTime unloadedAt = dto.getCduUnloadedAt() != null + ? dto.getCduUnloadedAt() : LocalDateTime.now(); + + CustomerDailyUco entity = customerDailyUcoRepository + .findByCduDriverIdAndCduDate(driverId, dto.getCduDate()) + .orElseGet(() -> CustomerDailyUco.builder() + .cduDate(dto.getCduDate()) + .cduDriverId(driverId) + .cduCreatedBy(dto.getCduLoginUser()) + .build()); + + entity.setCduActualQty(dto.getCduActualQty()); + entity.setCduActualWeight(dto.getCduActualWeight()); + entity.setCduUnloadedAt(unloadedAt); + entity.setCduUnloadPhoto(dto.getCduUnloadPhoto()); + if (dto.getCduNote() != null) { + entity.setCduNote(dto.getCduNote()); + } + entity.setCduUpdatedBy(dto.getCduLoginUser()); + entity.setCduUpdatedAt(LocalDateTime.now()); // UPDATE 시 DB default 안 걸리므로 명시 + + return toResponseDto(customerDailyUcoRepository.save(entity)); + } + + private CustomerDailyUcoResponseDto toResponseDto(CustomerDailyUco e) { + return CustomerDailyUcoResponseDto.builder() + .cduUuid(e.getCduUuid()) + .cduDate(e.getCduDate()) + .cduDriverId(e.getCduDriverId()) + .cduTargetQty(e.getCduTargetQty()) + .cduTargetVisitCnt(e.getCduTargetVisitCnt()) + .cduTargetSyncedAt(e.getCduTargetSyncedAt()) + .cduActualQty(e.getCduActualQty()) + .cduActualWeight(e.getCduActualWeight()) + .cduUnloadedAt(e.getCduUnloadedAt()) + .cduUnloadPhoto(e.getCduUnloadPhoto()) + .cduNote(e.getCduNote()) + .cduCreatedAt(e.getCduCreatedAt()) + .cduCreatedBy(e.getCduCreatedBy()) + .cduUpdatedAt(e.getCduUpdatedAt()) + .cduUpdatedBy(e.getCduUpdatedBy()) + .build(); + } + + private Long resolveDriverId(CustomerDailyUcoRequestDto dto) { + + // 1. driver id 직접 전달 + if (dto.getCduDriverId() != null) { + return dto.getCduDriverId(); + } + + // 2. MIS → externalId (UCO 는 항상 MIS_DRIVER) + if (dto.getCduExternalDriverId() != null) { + Map body = + hcmEmployeeClient.getEmpFromExternalId("MIS_DRIVER", dto.getCduExternalDriverId()); + + if (body != null && body.get("eexEmpId") != null) { + Object raw = body.get("eexEmpId"); + if (raw instanceof Number) { + return ((Number) raw).longValue(); + } + } + } + + // 3. ERP 내부 요청 (driverUuid 방식) + if (dto.getCduDriverUuid() != null) { + Map body = hcmEmployeeClient.getEmpFromUuid(dto.getCduDriverUuid()); + if (body != null && body.get("empId") != null) { + Object raw = body.get("empId"); + if (raw instanceof Number) { + return ((Number) raw).longValue(); + } + } + } + + return null; + } +} \ 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 index 2719efa..6753289 100644 --- a/src/main/java/com/goi/erp/service/DriverDailyStatService.java +++ b/src/main/java/com/goi/erp/service/DriverDailyStatService.java @@ -92,6 +92,11 @@ public class DriverDailyStatService { .ddsPausedAt(s.getDdsPausedAt()) .ddsEndAt(s.getDdsEndAt()) .ddsEndReason(s.getDdsEndReason()) + .ddsTargetQty(s.getDdsTargetQty()) + .ddsTargetCount(s.getDdsTargetCount()) + .ddsActualQty(s.getDdsActualQty()) + .ddsActualWeight(s.getDdsActualWeight()) + .ddsUnloadedAt(s.getDdsUnloadedAt()) .build(); } } \ No newline at end of file