목표량 취합을 위한 웹서비스 추가.

This commit is contained in:
Hyojin Ahn 2026-06-18 16:24:19 -04:00
parent fba94438fa
commit e7faffa262
9 changed files with 383 additions and 1 deletions

View File

@ -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<List<CustomerDailyUcoResponseDto>> getByDate(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
return ResponseEntity.ok(customerDailyUcoService.getByDate(date));
}
/** 단일 드라이버 + 일자 */
@GetMapping("/driver")
public ResponseEntity<CustomerDailyUcoResponseDto> 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<Void> syncTarget(@RequestBody CustomerDailyUcoRequestDto dto) {
customerDailyUcoService.syncTarget(dto);
return ResponseEntity.ok().build();
}
/** 드라이버 언로딩 입력 (실측 + 사진) */
@PutMapping("/unload")
public ResponseEntity<CustomerDailyUcoResponseDto> recordUnload(@RequestBody CustomerDailyUcoRequestDto dto) {
return ResponseEntity.ok(customerDailyUcoService.recordUnload(dto));
}
}

View File

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

View File

@ -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; // 수정자
}

View File

@ -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 = 미입력)
}

View File

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

View File

@ -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 = 미입력)
}

View File

@ -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<CustomerDailyUco, Long> {
// 조회 (대시보드 / 드릴다운)
List<CustomerDailyUco> findByCduDate(LocalDate cduDate);
Optional<CustomerDailyUco> findByCduDriverIdAndCduDate(Long cduDriverId, LocalDate cduDate);
List<CustomerDailyUco> 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);
}

View File

@ -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<CustomerDailyUcoResponseDto> 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<String, Object> 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<String, Object> 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;
}
}

View File

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