[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.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
|
@ -17,6 +18,7 @@ import com.goi.erp.token.PermissionAuthenticationToken;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class HcmEmployeeClient {
|
public class HcmEmployeeClient {
|
||||||
|
|
@ -62,8 +64,10 @@ public class HcmEmployeeClient {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 필요하면 logging
|
log.error(
|
||||||
System.out.println("externalId lookup error: " + e.getMessage());
|
"[EXTERNAL][GET] externalId lookup error: {}",
|
||||||
|
e.getMessage()
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +109,10 @@ public class HcmEmployeeClient {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 필요하면 로깅
|
log.error(
|
||||||
System.out.println("UUID lookup error: " + e.getMessage());
|
"[EXTERNAL][GET] UUID lookup error: {}",
|
||||||
|
e.getMessage()
|
||||||
|
);
|
||||||
return null;
|
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;
|
package com.goi.erp.controller;
|
||||||
|
|
||||||
import com.goi.erp.service.ExtSamsaraInspectionProcessor;
|
import com.goi.erp.service.ExtSamsaraInspectionProcessor;
|
||||||
|
import com.goi.erp.service.VehicleDispatchAutoCloseService;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
@ -14,10 +17,22 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
public class TestController {
|
public class TestController {
|
||||||
|
|
||||||
private final ExtSamsaraInspectionProcessor processor;
|
private final ExtSamsaraInspectionProcessor processor;
|
||||||
|
private final VehicleDispatchAutoCloseService autoCloseService;
|
||||||
|
|
||||||
@PostMapping("/process-samsara-inspection")
|
@PostMapping("/process-samsara-inspection")
|
||||||
public String process() {
|
public String process() {
|
||||||
processor.processUnprocessed();
|
processor.processUnprocessed();
|
||||||
return "OK";
|
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.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/vehicle-dispatch")
|
@RequestMapping("/vehicle-dispatch")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VehicleDispatchController {
|
public class VehicleDispatchController {
|
||||||
|
|
||||||
|
|
@ -234,6 +235,31 @@ public class VehicleDispatchController {
|
||||||
dispatchRepository.findByVedDispatchDate(date)
|
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
|
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 Integer vedShift;
|
||||||
private String vedStatus;
|
private String vedStatus;
|
||||||
private LocalDateTime vedStartAt;
|
private LocalDateTime vedStartAt;
|
||||||
|
private String vedStartReason;
|
||||||
private LocalDateTime vedPausedAt;
|
private LocalDateTime vedPausedAt;
|
||||||
private LocalDateTime vedEndAt;
|
private LocalDateTime vedEndAt;
|
||||||
private String vedEndReason;
|
private String vedEndReason;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public class VehicleDispatchResponseDto {
|
||||||
private LocalDate vedDispatchDate;
|
private LocalDate vedDispatchDate;
|
||||||
private Integer vedShift;
|
private Integer vedShift;
|
||||||
private LocalDateTime vedStartAt;
|
private LocalDateTime vedStartAt;
|
||||||
|
private String vedStartReason;
|
||||||
private LocalDateTime vedPausedAt;
|
private LocalDateTime vedPausedAt;
|
||||||
private LocalDateTime vedEndAt;
|
private LocalDateTime vedEndAt;
|
||||||
private String vedEndReason;
|
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")
|
@Column(name = "ved_start_at")
|
||||||
private LocalDateTime vedStartAt;
|
private LocalDateTime vedStartAt;
|
||||||
|
|
||||||
|
@Column(name = "ved_start_reason")
|
||||||
|
private String vedStartReason;
|
||||||
|
|
||||||
@Column(name = "ved_paused_at")
|
@Column(name = "ved_paused_at")
|
||||||
private LocalDateTime vedPausedAt;
|
private LocalDateTime vedPausedAt;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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 java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
@ -27,9 +28,19 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
||||||
SELECT d
|
SELECT d
|
||||||
FROM VehicleDispatch d
|
FROM VehicleDispatch d
|
||||||
WHERE d.vedVehId = :vehId
|
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("""
|
@Query("""
|
||||||
|
|
@ -54,4 +65,26 @@ public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch
|
||||||
Integer max = findMaxShift(vehId, date);
|
Integer max = findMaxShift(vehId, date);
|
||||||
return max == null ? 0 : max + 1;
|
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.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
|
@ -14,4 +15,11 @@ public interface VehicleExternalMapRepository extends JpaRepository<VehicleExter
|
||||||
String vexExternalId,
|
String vexExternalId,
|
||||||
String vexStatus
|
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 com.goi.erp.entity.VehicleInspection;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
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;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
|
@ -13,4 +15,13 @@ public interface VehicleInspectionRepository extends JpaRepository<VehicleInspec
|
||||||
|
|
||||||
Optional<VehicleInspection> findByVeiSourceAndVeiSourceId(String veiSource, Long veiSourceId);
|
Optional<VehicleInspection> findByVeiSourceAndVeiSourceId(String veiSource, Long veiSourceId);
|
||||||
Optional<VehicleInspection> findByVeiVehIdAndVeiInspectionDate(Long veiVehId, LocalDate veiInspectionDate);
|
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.VehicleDispatch;
|
||||||
import com.goi.erp.entity.VehicleInspection;
|
import com.goi.erp.entity.VehicleInspection;
|
||||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||||
|
import com.goi.erp.repository.VehicleInspectionRepository;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
|
@ -15,6 +17,8 @@ import java.time.Duration;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|
@ -23,6 +27,14 @@ import java.util.UUID;
|
||||||
public class VehicleDispatchService {
|
public class VehicleDispatchService {
|
||||||
|
|
||||||
private final VehicleDispatchRepository dispatchRepository;
|
private final VehicleDispatchRepository dispatchRepository;
|
||||||
|
private final VehicleInspectionRepository inspectionRepository;
|
||||||
|
// 누락된 재배차 로직
|
||||||
|
public enum RedispatchAction {
|
||||||
|
CREATED,
|
||||||
|
LINKED,
|
||||||
|
SKIPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===============================
|
/* ===============================
|
||||||
* CREATE
|
* CREATE
|
||||||
|
|
@ -34,7 +46,8 @@ public class VehicleDispatchService {
|
||||||
LocalDate dispatchDate,
|
LocalDate dispatchDate,
|
||||||
LocalDateTime startAt,
|
LocalDateTime startAt,
|
||||||
BigDecimal startOdometer,
|
BigDecimal startOdometer,
|
||||||
String source
|
String source,
|
||||||
|
String startReason
|
||||||
) {
|
) {
|
||||||
Integer nextShift = dispatchRepository.findNextShift(vehId, dispatchDate);
|
Integer nextShift = dispatchRepository.findNextShift(vehId, dispatchDate);
|
||||||
|
|
||||||
|
|
@ -48,6 +61,7 @@ public class VehicleDispatchService {
|
||||||
.vedStartAt(startAt)
|
.vedStartAt(startAt)
|
||||||
.vedOdometerStart(startOdometer)
|
.vedOdometerStart(startOdometer)
|
||||||
.vedStatus("O")
|
.vedStatus("O")
|
||||||
|
.vedStartReason(source != null ? source : "PRE_INSPECTION")
|
||||||
.vedSource(source != null ? source : "AUTO")
|
.vedSource(source != null ? source : "AUTO")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
|
@ -58,9 +72,7 @@ public class VehicleDispatchService {
|
||||||
VehicleDispatchRequestDto dto,
|
VehicleDispatchRequestDto dto,
|
||||||
String createdBy
|
String createdBy
|
||||||
) {
|
) {
|
||||||
VehicleDispatch opened =
|
VehicleDispatch opened = dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId(), dto.getVedDispatchDate()).orElse(null);
|
||||||
dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId())
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
if (opened != null) return opened;
|
if (opened != null) return opened;
|
||||||
|
|
||||||
|
|
@ -137,9 +149,7 @@ public class VehicleDispatchService {
|
||||||
* =============================== */
|
* =============================== */
|
||||||
public void evaluateDispatchCloseByInspection(VehicleInspection inspection) {
|
public void evaluateDispatchCloseByInspection(VehicleInspection inspection) {
|
||||||
|
|
||||||
VehicleDispatch open =
|
VehicleDispatch open = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId(), inspection.getVeiInspectionDate()).orElse(null);
|
||||||
dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId())
|
|
||||||
.orElse(null);
|
|
||||||
|
|
||||||
if (open == null) {
|
if (open == null) {
|
||||||
log.info(
|
log.info(
|
||||||
|
|
@ -259,6 +269,112 @@ public class VehicleDispatchService {
|
||||||
|
|
||||||
return dispatchRepository.save(d);
|
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 com.goi.erp.repository.VehicleExternalMapRepository;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class VehicleExternalMapService {
|
public class VehicleExternalMapService {
|
||||||
|
|
@ -15,8 +22,7 @@ public class VehicleExternalMapService {
|
||||||
private final VehicleExternalMapRepository externalMapRepository;
|
private final VehicleExternalMapRepository externalMapRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 외부 솔루션의 ID로 내부 employee 매핑 정보 조회
|
* 외부 솔루션의 ID로 내부 vehicle 매핑 정보 조회 (단건)
|
||||||
* 목적: customer_daily_order.driver_id 에 들어갈 내부 emp_id 조회
|
|
||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public VehicleExternalMapResponseDto findMapping(String solutionType, String externalId) {
|
public VehicleExternalMapResponseDto findMapping(String solutionType, String externalId) {
|
||||||
|
|
@ -28,10 +34,11 @@ public class VehicleExternalMapService {
|
||||||
"A" // 활성 매핑만 사용
|
"A" // 활성 매핑만 사용
|
||||||
)
|
)
|
||||||
.orElseThrow(() ->
|
.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();
|
VehicleExternalMapResponseDto dto = new VehicleExternalMapResponseDto();
|
||||||
dto.setVexUuid(map.getVexUuid());
|
dto.setVexUuid(map.getVexUuid());
|
||||||
dto.setVexVehicleId(map.getVexVehicleId());
|
dto.setVexVehicleId(map.getVexVehicleId());
|
||||||
|
|
@ -43,4 +50,46 @@ public class VehicleExternalMapService {
|
||||||
|
|
||||||
return dto;
|
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;
|
package com.goi.erp.service;
|
||||||
|
|
||||||
|
import com.goi.erp.client.HcmEmployeeClient;
|
||||||
import com.goi.erp.dto.VehicleInspectionDefectDto;
|
import com.goi.erp.dto.VehicleInspectionDefectDto;
|
||||||
import com.goi.erp.dto.VehicleInspectionDto;
|
import com.goi.erp.dto.VehicleInspectionDto;
|
||||||
import com.goi.erp.entity.VehicleDispatch;
|
import com.goi.erp.entity.VehicleDispatch;
|
||||||
|
|
@ -90,7 +91,7 @@ public class VehicleInspectionService {
|
||||||
// - odometerStart = inspection.odometer (가능하면)
|
// - odometerStart = inspection.odometer (가능하면)
|
||||||
if (shouldCreateDispatch(inspection)) {
|
if (shouldCreateDispatch(inspection)) {
|
||||||
// open 여부
|
// open 여부
|
||||||
boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId()).isPresent();
|
boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId(), inspection.getVeiInspectionDate()).isPresent();
|
||||||
|
|
||||||
if (!hasOpenDispatch) {
|
if (!hasOpenDispatch) {
|
||||||
|
|
||||||
|
|
@ -107,7 +108,8 @@ public class VehicleInspectionService {
|
||||||
inspection.getVeiInspectionDate(),
|
inspection.getVeiInspectionDate(),
|
||||||
inspection.getVeiStartAt(),
|
inspection.getVeiStartAt(),
|
||||||
odometerStart,
|
odometerStart,
|
||||||
"AUTO"
|
"AUTO",
|
||||||
|
"PRE_INSPECTION"
|
||||||
);
|
);
|
||||||
|
|
||||||
// inspection에 dispatch 연결
|
// inspection에 dispatch 연결
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ server:
|
||||||
hcm:
|
hcm:
|
||||||
api:
|
api:
|
||||||
base-url: http://localhost:8081/hcm-rest-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:
|
internal:
|
||||||
integration:
|
integration:
|
||||||
token: ${INTEGRATION_INTERNAL_TOKEN}
|
token: ${INTEGRATION_INTERNAL_TOKEN}
|
||||||
Loading…
Reference in New Issue