[VehicleDispatch]
- Added dispatch data from inspection - Added AutoClose to auto close idle + home vehicles [Config] - Added get configurations from sys-rest-api
This commit is contained in:
parent
3347de524f
commit
923b6d5aca
|
|
@ -1,6 +1,7 @@
|
|||
package com.goi.erp.service;
|
||||
package com.goi.erp.client;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
|
|
@ -17,6 +18,7 @@ import com.goi.erp.token.PermissionAuthenticationToken;
|
|||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HcmEmployeeClient {
|
||||
|
|
@ -62,8 +64,10 @@ public class HcmEmployeeClient {
|
|||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 필요하면 logging
|
||||
System.out.println("externalId lookup error: " + e.getMessage());
|
||||
log.error(
|
||||
"[EXTERNAL][GET] externalId lookup error: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -105,8 +109,10 @@ public class HcmEmployeeClient {
|
|||
return null;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 필요하면 로깅
|
||||
System.out.println("UUID lookup error: " + e.getMessage());
|
||||
log.error(
|
||||
"[EXTERNAL][GET] UUID lookup error: {}",
|
||||
e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.goi.erp.client;
|
||||
|
||||
import com.goi.erp.dto.VehicleStatGpsResponseDto;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SamsaraVehicleStatGpsClient {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SamsaraVehicleStatGpsClient(
|
||||
WebClient.Builder webClientBuilder,
|
||||
@Value("${integration.api.base-url}") String integrationBaseUrl
|
||||
) {
|
||||
this.webClient = webClientBuilder
|
||||
.baseUrl(integrationBaseUrl)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* integration-service 를 통해 Samsara vehicle engine + GPS 상태 조회
|
||||
*/
|
||||
public List<VehicleStatGpsResponseDto> fetchVehicleStats(List<String> vehicleExternalIds) {
|
||||
|
||||
if (vehicleExternalIds == null || vehicleExternalIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
try {
|
||||
// log.info(
|
||||
// "vehicleIds={}",
|
||||
// vehicleExternalIds
|
||||
// );
|
||||
return webClient.get()
|
||||
.uri(uriBuilder -> {
|
||||
uriBuilder
|
||||
.path("/vehicle/stat/gps");
|
||||
vehicleExternalIds.forEach(
|
||||
id -> uriBuilder.queryParam("vehicleIds", id)
|
||||
);
|
||||
return uriBuilder.build();
|
||||
})
|
||||
.retrieve()
|
||||
.bodyToFlux(VehicleStatGpsResponseDto.class)
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(10)); // 10초 타임아웃
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to fetch vehicle stats from integration-service. vehicleIds={}",
|
||||
vehicleExternalIds,
|
||||
e
|
||||
);
|
||||
return List.of(); // 장애 시 전체 skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package com.goi.erp.client;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.goi.erp.dto.SysConfigResponseDto;
|
||||
import com.goi.erp.token.PermissionAuthenticationToken;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SysConfigClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
@Value("${sys.api.base-url}")
|
||||
private String sysBaseUrl;
|
||||
|
||||
public SysConfigResponseDto getRaw(String module, String key) {
|
||||
|
||||
String url = sysBaseUrl + "/config/" + module + "/" + key;
|
||||
|
||||
try {
|
||||
HttpEntity<Void> entity = new HttpEntity<>(buildHeaders());
|
||||
|
||||
ResponseEntity<SysConfigResponseDto> response =
|
||||
restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
SysConfigResponseDto.class
|
||||
);
|
||||
|
||||
return response.getBody();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"[SYS-CONFIG][GET] {}.{} failed: {}",
|
||||
module, key, e.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getString(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "STRING");
|
||||
return cfg.getCfgValue();
|
||||
}
|
||||
|
||||
public Integer getInt(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "INT");
|
||||
return Integer.valueOf(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
public Long getLong(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "INT"); // 정책: INT = Long 허용
|
||||
return Long.valueOf(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
public BigDecimal getDecimal(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "DECIMAL");
|
||||
return new BigDecimal(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
public Boolean getBoolean(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "BOOL");
|
||||
return Boolean.valueOf(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
public LocalDate getDate(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "DATE");
|
||||
return LocalDate.parse(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
public LocalDateTime getDateTime(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRequired(module, key);
|
||||
assertType(cfg, "DATETIME");
|
||||
return LocalDateTime.parse(cfg.getCfgValue());
|
||||
}
|
||||
|
||||
//
|
||||
private SysConfigResponseDto getRequired(String module, String key) {
|
||||
SysConfigResponseDto cfg = getRaw(module, key);
|
||||
if (cfg == null) {
|
||||
log.error(
|
||||
"[SYS-CONFIG][GET] {}.{} failed: {}",
|
||||
module, key, "Config not found: " + module + "." + key
|
||||
);
|
||||
throw new IllegalStateException(
|
||||
"Config not found: " + module + "." + key
|
||||
);
|
||||
}
|
||||
if (cfg.getCfgValue() == null) {
|
||||
log.error(
|
||||
"[SYS-CONFIG][GET] {}.{} failed: {}",
|
||||
module, key, "Config value is null: " + module + "." + key
|
||||
);
|
||||
throw new IllegalStateException(
|
||||
"Config value is null: " + module + "." + key
|
||||
);
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private void assertType(SysConfigResponseDto cfg, String expected) {
|
||||
String actual = cfg.getCfgDataType();
|
||||
|
||||
if (actual == null) {
|
||||
throw new IllegalStateException(
|
||||
"Config data type is null: " +
|
||||
cfg.getCfgModule() + "." + cfg.getCfgKey()
|
||||
);
|
||||
}
|
||||
|
||||
if (!expected.equalsIgnoreCase(actual)) {
|
||||
throw new IllegalStateException(
|
||||
"Config type mismatch: " +
|
||||
cfg.getCfgModule() + "." + cfg.getCfgKey() +
|
||||
" expected=" + expected +
|
||||
" actual=" + actual
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private HttpHeaders buildHeaders() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
String jwt = getCurrentJwt();
|
||||
if (jwt != null) {
|
||||
headers.set("Authorization", "Bearer " + jwt);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private String getCurrentJwt() {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof PermissionAuthenticationToken token) {
|
||||
return token.getJwt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.goi.erp.common.util;
|
||||
|
||||
public final class GeoUtil {
|
||||
|
||||
private static final double EARTH_RADIUS_METERS = 6_371_000;
|
||||
|
||||
private GeoUtil() {
|
||||
// utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 좌표 간 거리 (meters)
|
||||
*/
|
||||
public static double distanceMeters(
|
||||
double lat1,
|
||||
double lon1,
|
||||
double lat2,
|
||||
double lon2
|
||||
) {
|
||||
|
||||
double dLat = Math.toRadians(lat2 - lat1);
|
||||
double dLon = Math.toRadians(lon2 - lon1);
|
||||
|
||||
double a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2)
|
||||
+ Math.cos(Math.toRadians(lat1))
|
||||
* Math.cos(Math.toRadians(lat2))
|
||||
* Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return EARTH_RADIUS_METERS * c;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package com.goi.erp.controller;
|
||||
|
||||
import com.goi.erp.service.ExtSamsaraInspectionProcessor;
|
||||
import com.goi.erp.service.VehicleDispatchAutoCloseService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
|
@ -14,10 +17,22 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
public class TestController {
|
||||
|
||||
private final ExtSamsaraInspectionProcessor processor;
|
||||
private final VehicleDispatchAutoCloseService autoCloseService;
|
||||
|
||||
@PostMapping("/process-samsara-inspection")
|
||||
public String process() {
|
||||
processor.processUnprocessed();
|
||||
return "OK";
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 배차 자동 종료 테스트 (오늘 날짜 기준)
|
||||
*/
|
||||
@PostMapping("/auto-close-dispatch")
|
||||
public String autoCloseDispatch() {
|
||||
|
||||
autoCloseService.processAutoClose(LocalDate.now());
|
||||
|
||||
return "AUTO CLOSE OK";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ import java.time.Duration;
|
|||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vehicle-dispatch")
|
||||
@RequestMapping("/vehicle-dispatch")
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleDispatchController {
|
||||
|
||||
|
|
@ -234,6 +235,31 @@ public class VehicleDispatchController {
|
|||
dispatchRepository.findByVedDispatchDate(date)
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
누락된 dispatch 찾아서 재 배차 /redispatch?date=2025-12-24
|
||||
============================================================ */
|
||||
@PostMapping("/redispatch")
|
||||
public ResponseEntity<Map<String, Object>> redispatch(
|
||||
@RequestParam LocalDate date
|
||||
) {
|
||||
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.canUpdateOPR(permissionSet)) {
|
||||
throw new AccessDeniedException("You do not have permission to redispatch");
|
||||
}
|
||||
|
||||
Map<String, Object> result = dispatchService.redispatchByDate(date);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
JOB TRIGGER
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class SysConfigResponseDto {
|
||||
|
||||
private Long cfgId;
|
||||
private UUID cfgUuid;
|
||||
|
||||
private String cfgModule;
|
||||
private String cfgKey;
|
||||
private String cfgValue;
|
||||
private String cfgDataType;
|
||||
|
||||
private LocalDateTime cfgCreatedAt;
|
||||
private LocalDateTime cfgUpdatedAt;
|
||||
private String cfgCreatedBy;
|
||||
private String cfgUpdatedBy;
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ public class VehicleDispatchRequestDto {
|
|||
private Integer vedShift;
|
||||
private String vedStatus;
|
||||
private LocalDateTime vedStartAt;
|
||||
private String vedStartReason;
|
||||
private LocalDateTime vedPausedAt;
|
||||
private LocalDateTime vedEndAt;
|
||||
private String vedEndReason;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ public class VehicleDispatchResponseDto {
|
|||
private LocalDate vedDispatchDate;
|
||||
private Integer vedShift;
|
||||
private LocalDateTime vedStartAt;
|
||||
private String vedStartReason;
|
||||
private LocalDateTime vedPausedAt;
|
||||
private LocalDateTime vedEndAt;
|
||||
private String vedEndReason;
|
||||
|
|
|
|||
|
|
@ -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.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class VehicleStatGpsResponseDto {
|
||||
|
||||
private String vesVehicleExternalId; // Samsara vehicle external ID
|
||||
private Boolean vesEngineOn; // Engine state: true = On, false = Off, null = unknown
|
||||
private BigDecimal vesLatitude; // Latest GPS latitude
|
||||
private BigDecimal vesLongitude; // Latest GPS longitude
|
||||
private Instant vesGpsTime; // GPS timestamp (UTC, from Samsara)
|
||||
}
|
||||
|
|
@ -66,6 +66,9 @@ public class VehicleDispatch {
|
|||
@Column(name = "ved_start_at")
|
||||
private LocalDateTime vedStartAt;
|
||||
|
||||
@Column(name = "ved_start_reason")
|
||||
private String vedStartReason;
|
||||
|
||||
@Column(name = "ved_paused_at")
|
||||
private LocalDateTime vedPausedAt;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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 java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -27,9 +28,19 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
|||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedVehId = :vehId
|
||||
AND d.vedStatus IN ('O','P')
|
||||
AND d.vedDispatchDate = :dispatchDate
|
||||
AND d.vedStatus <> 'C'
|
||||
""")
|
||||
Optional<VehicleDispatch> findOpenDispatchByVehId(Long vehId);
|
||||
Optional<VehicleDispatch> findOpenDispatchByVehId(Long vehId, LocalDate dispatchDate);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedDispatchDate = :dispatchDate
|
||||
AND d.vedStatus <> 'C'
|
||||
""")
|
||||
List<VehicleDispatch> findAllNotClosed(LocalDate dispatchDate);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
|
|
@ -54,4 +65,26 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
|||
Integer max = findMaxShift(vehId, date);
|
||||
return max == null ? 0 : max + 1;
|
||||
}
|
||||
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedDispatchDate = :dispatchDate
|
||||
AND d.vedStatus = 'C'
|
||||
AND d.vedEndReason = :closeReason
|
||||
AND d.vedEndAt >= :closedAfter
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM VehicleDispatch x
|
||||
WHERE x.vedVehId = d.vedVehId
|
||||
AND x.vedDispatchDate = :dispatchDate
|
||||
AND x.vedStatus <> 'C'
|
||||
)
|
||||
|
||||
""")
|
||||
List<VehicleDispatch> findAutoClosedCandidatesForReopen(
|
||||
@Param("dispatchDate") LocalDate dispatchDate,
|
||||
@Param("closeReason") String closeReason,
|
||||
@Param("closedAfter") LocalDateTime closedAfter
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.goi.erp.entity.VehicleExternalMap;
|
|||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
|
|
@ -14,4 +15,11 @@ public interface VehicleExternalMapRepository extends JpaRepository<VehicleExter
|
|||
String vexExternalId,
|
||||
String vexStatus
|
||||
);
|
||||
|
||||
List<VehicleExternalMap> findAllByVexSolutionTypeAndVexVehicleIdInAndVexStatus(
|
||||
String solutionType,
|
||||
List<Long> vexVehicleId,
|
||||
String vexStatus
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.goi.erp.repository;
|
|||
import com.goi.erp.entity.VehicleInspection;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
|
|
@ -13,4 +15,13 @@ public interface VehicleInspectionRepository extends JpaRepository<VehicleInspec
|
|||
|
||||
Optional<VehicleInspection> findByVeiSourceAndVeiSourceId(String veiSource, Long veiSourceId);
|
||||
Optional<VehicleInspection> findByVeiVehIdAndVeiInspectionDate(Long veiVehId, LocalDate veiInspectionDate);
|
||||
|
||||
@Query("""
|
||||
select i
|
||||
from VehicleInspection i
|
||||
where i.veiInspectionType = 'preTrip'
|
||||
and i.veiInspectionDate = :date
|
||||
and i.veiDispatchId is null
|
||||
""")
|
||||
List<VehicleInspection> findUnlinkedPreTripByDate(LocalDate date);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.client.SamsaraVehicleStatGpsClient;
|
||||
import com.goi.erp.client.SysConfigClient;
|
||||
import com.goi.erp.common.util.GeoUtil;
|
||||
import com.goi.erp.dto.VehicleStatGpsResponseDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleDispatchAutoCloseService {
|
||||
/*
|
||||
* O → P : HOME 근접 + engine OFF
|
||||
* P → C : HOME 근접 + engine OFF + paused_at ≥ N분
|
||||
*/
|
||||
|
||||
private final VehicleDispatchRepository vehicleDispatchRepository;
|
||||
private final VehicleExternalMapService vehicleExternalMapService;
|
||||
private final SamsaraVehicleStatGpsClient vehicleStatClient;
|
||||
private final SysConfigClient sysConfigClient;
|
||||
|
||||
@Transactional
|
||||
public void processAutoClose(LocalDate dispatchDate) {
|
||||
// config
|
||||
BigDecimal homeLat = sysConfigClient.getDecimal("SYS", "HOME_LATITUDE");
|
||||
BigDecimal homeLon = sysConfigClient.getDecimal("SYS", "HOME_LONGITUDE");
|
||||
Integer radiusMeters = sysConfigClient.getInt("OPR", "HOME_RADIUS_METERS");
|
||||
Integer pausedMinutes = sysConfigClient.getInt("OPR", "PAUSED_CLOSE_MINUTES");
|
||||
|
||||
// 0. auto open check 먼저
|
||||
processAutoOpen(dispatchDate, homeLat, homeLon, radiusMeters);
|
||||
|
||||
// 1. 대상 vehicle_dispatch 조회 (C 제외)
|
||||
List<VehicleDispatch> targets = vehicleDispatchRepository.findAllNotClosed(dispatchDate);
|
||||
if (targets.isEmpty()) return;
|
||||
|
||||
// 2. external vehicleIds 수집
|
||||
List<Long> vehicleIds = targets.stream()
|
||||
.map(VehicleDispatch::getVedVehId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (vehicleIds.isEmpty()) return;
|
||||
|
||||
|
||||
// 3. internal vehicleId -> samsara externalId 매핑 bulk 조회
|
||||
Map<Long, String> vehicleIdToExternalId = vehicleExternalMapService.findExternalIdsByVehicleIds("SAMSARA", vehicleIds);
|
||||
|
||||
List<String> externalIds = vehicleIdToExternalId.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (externalIds.isEmpty()) return;
|
||||
|
||||
// 4. call
|
||||
List<VehicleStatGpsResponseDto> stats = vehicleStatClient.fetchVehicleStats(externalIds);
|
||||
if (stats.isEmpty()) return;
|
||||
|
||||
// 5. response
|
||||
Map<String, VehicleStatGpsResponseDto> statMap = stats.stream()
|
||||
.collect(Collectors.toMap(
|
||||
VehicleStatGpsResponseDto::getVesVehicleExternalId,
|
||||
s -> s,
|
||||
(a, b) -> a
|
||||
));
|
||||
|
||||
// 6. dispatch별 처리
|
||||
for (VehicleDispatch dispatch : targets) {
|
||||
|
||||
Long internalVehicleId = dispatch.getVedVehId();
|
||||
if (internalVehicleId == null) continue;
|
||||
|
||||
String externalId = vehicleIdToExternalId.get(internalVehicleId);
|
||||
if (externalId == null) continue;
|
||||
|
||||
VehicleStatGpsResponseDto stat = statMap.get(externalId);
|
||||
if (stat == null) continue;
|
||||
|
||||
// 관측값 계산
|
||||
boolean engineOn = Boolean.TRUE.equals(stat.getVesEngineOn());
|
||||
|
||||
double distanceMeters = GeoUtil.distanceMeters(
|
||||
homeLat.doubleValue(),
|
||||
homeLon.doubleValue(),
|
||||
stat.getVesLatitude().doubleValue(),
|
||||
stat.getVesLongitude().doubleValue()
|
||||
);
|
||||
|
||||
boolean isAtHome = distanceMeters <= radiusMeters;
|
||||
boolean isStoppedAtHome = !engineOn && isAtHome;
|
||||
|
||||
// 상태 변경
|
||||
String status = dispatch.getVedStatus();
|
||||
|
||||
// P → O (다시 출발)
|
||||
if ("P".equals(status) && !isStoppedAtHome) {
|
||||
transitionToOnRoute(dispatch);
|
||||
continue;
|
||||
}
|
||||
|
||||
// O -> P (도착 후 정차)
|
||||
if ("O".equals(status) && isStoppedAtHome) {
|
||||
transitionToPaused(dispatch);
|
||||
continue;
|
||||
}
|
||||
|
||||
// P -> C (장시간 정차)
|
||||
if ("P".equals(status) && isStoppedAtHome) {
|
||||
|
||||
if (dispatch.getVedPausedAt() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long pausedMin = Duration.between(
|
||||
dispatch.getVedPausedAt(),
|
||||
LocalDateTime.now()
|
||||
).toMinutes();
|
||||
|
||||
if (pausedMin >= pausedMinutes) {
|
||||
transitionToClosed(dispatch, "AUTO_CLOSE_HOME_IDLE");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processAutoOpen(
|
||||
LocalDate dispatchDate,
|
||||
BigDecimal homeLat,
|
||||
BigDecimal homeLon,
|
||||
Integer radiusMeters
|
||||
) {
|
||||
|
||||
// AUTO_OPEN 시간 윈도우 (예: 최근 2시간)
|
||||
LocalDateTime closedAfter = LocalDateTime.now().minusHours(2);
|
||||
|
||||
// 1. AUTO_CLOSE된 후보 조회
|
||||
List<VehicleDispatch> closedCandidates =
|
||||
vehicleDispatchRepository.findAutoClosedCandidatesForReopen(
|
||||
dispatchDate,
|
||||
"AUTO_CLOSE_HOME_IDLE",
|
||||
closedAfter
|
||||
);
|
||||
|
||||
if (closedCandidates.isEmpty()) return;
|
||||
|
||||
// 2. vehicleId 수집
|
||||
List<Long> vehicleIds = closedCandidates.stream()
|
||||
.map(VehicleDispatch::getVedVehId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (vehicleIds.isEmpty()) return;
|
||||
|
||||
// 3. externalId 매핑
|
||||
Map<Long, String> vehicleIdToExternalId =
|
||||
vehicleExternalMapService.findExternalIdsByVehicleIds(
|
||||
"SAMSARA",
|
||||
vehicleIds
|
||||
);
|
||||
|
||||
List<String> externalIds = vehicleIdToExternalId.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (externalIds.isEmpty()) return;
|
||||
|
||||
// 4. stats 조회
|
||||
List<VehicleStatGpsResponseDto> stats = vehicleStatClient.fetchVehicleStats(externalIds);
|
||||
if (stats.isEmpty()) return;
|
||||
|
||||
Map<String, VehicleStatGpsResponseDto> statMap = stats.stream()
|
||||
.collect(Collectors.toMap(
|
||||
VehicleStatGpsResponseDto::getVesVehicleExternalId,
|
||||
s -> s,
|
||||
(a, b) -> a
|
||||
));
|
||||
|
||||
// 5. AUTO-OPEN 판단
|
||||
for (VehicleDispatch closed : closedCandidates) {
|
||||
|
||||
Long vehicleId = closed.getVedVehId();
|
||||
String externalId = vehicleIdToExternalId.get(vehicleId);
|
||||
if (externalId == null) continue;
|
||||
|
||||
VehicleStatGpsResponseDto stat = statMap.get(externalId);
|
||||
if (stat == null) continue;
|
||||
|
||||
boolean engineOn = Boolean.TRUE.equals(stat.getVesEngineOn());
|
||||
|
||||
double distanceMeters = GeoUtil.distanceMeters(
|
||||
homeLat.doubleValue(),
|
||||
homeLon.doubleValue(),
|
||||
stat.getVesLatitude().doubleValue(),
|
||||
stat.getVesLongitude().doubleValue()
|
||||
);
|
||||
|
||||
boolean leftHome = distanceMeters > radiusMeters;
|
||||
|
||||
// ⭐ AUTO-OPEN 조건
|
||||
if (engineOn && leftHome) {
|
||||
|
||||
createNewDispatchFromAutoClose(closed);
|
||||
|
||||
log.info(
|
||||
"[AUTO-OPEN] New dispatch created from AUTO_CLOSE. prevDispatchId={}",
|
||||
closed.getVedId()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void transitionToOnRoute(VehicleDispatch dispatch) {
|
||||
dispatch.setVedStatus("O");
|
||||
dispatch.setVedPausedAt(null);
|
||||
vehicleDispatchRepository.save(dispatch);
|
||||
|
||||
log.info(
|
||||
"[AUTO-CLOSE] Dispatch {} → O (re-open)",
|
||||
dispatch.getVedId()
|
||||
);
|
||||
}
|
||||
|
||||
private void transitionToPaused(VehicleDispatch dispatch) {
|
||||
dispatch.setVedStatus("P");
|
||||
dispatch.setVedPausedAt(LocalDateTime.now());
|
||||
vehicleDispatchRepository.save(dispatch);
|
||||
|
||||
log.info(
|
||||
"[AUTO-CLOSE] Dispatch {} → P (paused)",
|
||||
dispatch.getVedId()
|
||||
);
|
||||
}
|
||||
|
||||
private void transitionToClosed(VehicleDispatch dispatch, String reason) {
|
||||
dispatch.setVedStatus("C");
|
||||
dispatch.setVedEndAt(LocalDateTime.now());
|
||||
dispatch.setVedEndReason(reason);
|
||||
vehicleDispatchRepository.save(dispatch);
|
||||
|
||||
log.info(
|
||||
"[AUTO-CLOSE] Dispatch {} → C (closed)",
|
||||
dispatch.getVedId()
|
||||
);
|
||||
}
|
||||
|
||||
private void createNewDispatchFromAutoClose(VehicleDispatch closed) {
|
||||
|
||||
VehicleDispatch newDispatch = VehicleDispatch.builder()
|
||||
.vedUuid(UUID.randomUUID())
|
||||
.vedDriverId(closed.getVedDriverId())
|
||||
.vedSubDriverId(closed.getVedSubDriverId())
|
||||
.vedVehId(closed.getVedVehId())
|
||||
.vedDispatchDate(closed.getVedDispatchDate())
|
||||
.vedShift(closed.getVedShift() + 1)
|
||||
.vedStartAt(LocalDateTime.now())
|
||||
.vedOdometerStart(closed.getVedOdometerEnd())
|
||||
.vedStatus("O")
|
||||
.vedSource("AUTO")
|
||||
.vedStartReason("AUTO_OPEN_LEAVE_HOME")
|
||||
.build();
|
||||
|
||||
vehicleDispatchRepository.save(newDispatch);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -4,7 +4,9 @@ import com.goi.erp.dto.VehicleDispatchRequestDto;
|
|||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.entity.VehicleInspection;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.repository.VehicleInspectionRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
|
@ -15,6 +17,8 @@ import java.time.Duration;
|
|||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -23,6 +27,14 @@ import java.util.UUID;
|
|||
public class VehicleDispatchService {
|
||||
|
||||
private final VehicleDispatchRepository dispatchRepository;
|
||||
private final VehicleInspectionRepository inspectionRepository;
|
||||
// 누락된 재배차 로직
|
||||
public enum RedispatchAction {
|
||||
CREATED,
|
||||
LINKED,
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
|
||||
/* ===============================
|
||||
* CREATE
|
||||
|
|
@ -34,7 +46,8 @@ public class VehicleDispatchService {
|
|||
LocalDate dispatchDate,
|
||||
LocalDateTime startAt,
|
||||
BigDecimal startOdometer,
|
||||
String source
|
||||
String source,
|
||||
String startReason
|
||||
) {
|
||||
Integer nextShift = dispatchRepository.findNextShift(vehId, dispatchDate);
|
||||
|
||||
|
|
@ -48,6 +61,7 @@ public class VehicleDispatchService {
|
|||
.vedStartAt(startAt)
|
||||
.vedOdometerStart(startOdometer)
|
||||
.vedStatus("O")
|
||||
.vedStartReason(source != null ? source : "PRE_INSPECTION")
|
||||
.vedSource(source != null ? source : "AUTO")
|
||||
.build();
|
||||
|
||||
|
|
@ -58,9 +72,7 @@ public class VehicleDispatchService {
|
|||
VehicleDispatchRequestDto dto,
|
||||
String createdBy
|
||||
) {
|
||||
VehicleDispatch opened =
|
||||
dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId())
|
||||
.orElse(null);
|
||||
VehicleDispatch opened = dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId(), dto.getVedDispatchDate()).orElse(null);
|
||||
|
||||
if (opened != null) return opened;
|
||||
|
||||
|
|
@ -137,9 +149,7 @@ public class VehicleDispatchService {
|
|||
* =============================== */
|
||||
public void evaluateDispatchCloseByInspection(VehicleInspection inspection) {
|
||||
|
||||
VehicleDispatch open =
|
||||
dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId())
|
||||
.orElse(null);
|
||||
VehicleDispatch open = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId(), inspection.getVeiInspectionDate()).orElse(null);
|
||||
|
||||
if (open == null) {
|
||||
log.info(
|
||||
|
|
@ -259,6 +269,112 @@ public class VehicleDispatchService {
|
|||
|
||||
return dispatchRepository.save(d);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Map<String, Object> redispatchByDate(LocalDate date) {
|
||||
|
||||
int total = 0;
|
||||
int created = 0;
|
||||
int linked = 0;
|
||||
int skipped = 0;
|
||||
|
||||
// 1. 해당 날짜 + 배차 안 된 preTrip inspection
|
||||
List<VehicleInspection> inspections =
|
||||
inspectionRepository.findUnlinkedPreTripByDate(date);
|
||||
|
||||
for (VehicleInspection inspection : inspections) {
|
||||
|
||||
total++;
|
||||
|
||||
try {
|
||||
RedispatchAction action = redispatchSingle(inspection);
|
||||
|
||||
switch (action) {
|
||||
case LINKED -> linked++;
|
||||
case CREATED -> created++;
|
||||
case SKIPPED -> skipped++;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
skipped++;
|
||||
log.error(
|
||||
"[REDISPATCH][FAIL] inspectionId={} vehId={} driverId={}",
|
||||
inspection.getVeiId(),
|
||||
inspection.getVeiVehId(),
|
||||
inspection.getVeiDriverId(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Map.of(
|
||||
"dispatchDate", date,
|
||||
"totalInspections", total,
|
||||
"created", created,
|
||||
"linked", linked,
|
||||
"skipped", skipped
|
||||
);
|
||||
}
|
||||
|
||||
private RedispatchAction redispatchSingle(VehicleInspection inspection) {
|
||||
|
||||
// 필수값 체크
|
||||
if (inspection.getVeiVehId() == null || inspection.getVeiDriverId() == null) {
|
||||
log.warn(
|
||||
"[REDISPATCH][SKIP] missing veh/driver inspectionId={}",
|
||||
inspection.getVeiId()
|
||||
);
|
||||
return RedispatchAction.SKIPPED;
|
||||
}
|
||||
|
||||
// 같은 날짜 + 차량 open dispatch 있는지
|
||||
Optional<VehicleDispatch> open = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId(), inspection.getVeiInspectionDate());
|
||||
|
||||
if (open.isPresent()) {
|
||||
inspection.setVeiDispatchId(open.get().getVedId());
|
||||
inspectionRepository.save(inspection);
|
||||
|
||||
log.info(
|
||||
"[REDISPATCH][LINK] inspectionId={} → dispatchId={}",
|
||||
inspection.getVeiId(),
|
||||
open.get().getVedId()
|
||||
);
|
||||
|
||||
return RedispatchAction.LINKED;
|
||||
}
|
||||
|
||||
// 없으면 새 dispatch 생성
|
||||
BigDecimal odometerStart =
|
||||
inspection.getVeiOdometer() != null
|
||||
? BigDecimal.valueOf(inspection.getVeiOdometer())
|
||||
: null;
|
||||
|
||||
VehicleDispatch dispatch =
|
||||
createFromPreTripInspection(
|
||||
inspection.getVeiVehId(),
|
||||
inspection.getVeiDriverId(),
|
||||
inspection.getVeiSubDriverId(),
|
||||
inspection.getVeiInspectionDate(),
|
||||
inspection.getVeiStartAt(),
|
||||
odometerStart,
|
||||
"REDISPATCH",
|
||||
"PRE_INSPECTION"
|
||||
);
|
||||
|
||||
inspection.setVeiDispatchId(dispatch.getVedId());
|
||||
inspectionRepository.save(inspection);
|
||||
|
||||
log.info(
|
||||
"[REDISPATCH][CREATE] inspectionId={} → dispatchId={}",
|
||||
inspection.getVeiId(),
|
||||
dispatch.getVedId()
|
||||
);
|
||||
|
||||
return RedispatchAction.CREATED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//
|
||||
|
|
|
|||
|
|
@ -5,9 +5,16 @@ import com.goi.erp.entity.VehicleExternalMap;
|
|||
import com.goi.erp.repository.VehicleExternalMapRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleExternalMapService {
|
||||
|
|
@ -15,8 +22,7 @@ public class VehicleExternalMapService {
|
|||
private final VehicleExternalMapRepository externalMapRepository;
|
||||
|
||||
/**
|
||||
* 외부 솔루션의 ID로 내부 employee 매핑 정보 조회
|
||||
* 목적: customer_daily_order.driver_id 에 들어갈 내부 emp_id 조회
|
||||
* 외부 솔루션의 ID로 내부 vehicle 매핑 정보 조회 (단건)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public VehicleExternalMapResponseDto findMapping(String solutionType, String externalId) {
|
||||
|
|
@ -28,10 +34,11 @@ public class VehicleExternalMapService {
|
|||
"A" // 활성 매핑만 사용
|
||||
)
|
||||
.orElseThrow(() ->
|
||||
new RuntimeException("Vehicle external mapping not found for externalId: " + externalId)
|
||||
new RuntimeException(
|
||||
"Vehicle external mapping not found for externalId: " + externalId
|
||||
)
|
||||
);
|
||||
|
||||
// Entity → ResponseDto 변환
|
||||
VehicleExternalMapResponseDto dto = new VehicleExternalMapResponseDto();
|
||||
dto.setVexUuid(map.getVexUuid());
|
||||
dto.setVexVehicleId(map.getVexVehicleId());
|
||||
|
|
@ -43,4 +50,46 @@ public class VehicleExternalMapService {
|
|||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 내부 vehicleId 목록으로 외부 솔루션 ID bulk 조회
|
||||
*
|
||||
* @return Map<internalVehicleId, externalId>
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Map<Long, String> findExternalIdsByVehicleIds(
|
||||
String solutionType,
|
||||
List<Long> vehicleIds
|
||||
) {
|
||||
|
||||
if (vehicleIds == null || vehicleIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
List<VehicleExternalMap> maps =
|
||||
externalMapRepository
|
||||
.findAllByVexSolutionTypeAndVexVehicleIdInAndVexStatus(
|
||||
solutionType,
|
||||
vehicleIds,
|
||||
"A" // 활성 매핑만
|
||||
);
|
||||
|
||||
if (maps.isEmpty()) {
|
||||
log.warn(
|
||||
"[VEHICLE-EXT-MAP] No active mappings found. solutionType={}, vehicleIds={}",
|
||||
solutionType,
|
||||
vehicleIds
|
||||
);
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
// internal vehicleId -> externalId
|
||||
return maps.stream()
|
||||
.filter(m -> m.getVexVehicleId() != null && m.getVexExternalId() != null)
|
||||
.collect(Collectors.toMap(
|
||||
VehicleExternalMap::getVexVehicleId,
|
||||
VehicleExternalMap::getVexExternalId,
|
||||
(a, b) -> a // 중복 시 첫 번째 유지
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.client.HcmEmployeeClient;
|
||||
import com.goi.erp.dto.VehicleInspectionDefectDto;
|
||||
import com.goi.erp.dto.VehicleInspectionDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
|
|
@ -90,7 +91,7 @@ public class VehicleInspectionService {
|
|||
// - odometerStart = inspection.odometer (가능하면)
|
||||
if (shouldCreateDispatch(inspection)) {
|
||||
// open 여부
|
||||
boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId()).isPresent();
|
||||
boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId(), inspection.getVeiInspectionDate()).isPresent();
|
||||
|
||||
if (!hasOpenDispatch) {
|
||||
|
||||
|
|
@ -107,7 +108,8 @@ public class VehicleInspectionService {
|
|||
inspection.getVeiInspectionDate(),
|
||||
inspection.getVeiStartAt(),
|
||||
odometerStart,
|
||||
"AUTO"
|
||||
"AUTO",
|
||||
"PRE_INSPECTION"
|
||||
);
|
||||
|
||||
// inspection에 dispatch 연결
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ server:
|
|||
hcm:
|
||||
api:
|
||||
base-url: http://localhost:8081/hcm-rest-api
|
||||
sys:
|
||||
api:
|
||||
base-url: http://localhost:8090/sys-rest-api
|
||||
integration:
|
||||
api:
|
||||
base-url: http://localhost:8091/integration-service
|
||||
internal:
|
||||
integration:
|
||||
token: ${INTEGRATION_INTERNAL_TOKEN}
|
||||
Loading…
Reference in New Issue