[DvirIngest]

- Changed naming
[Vehicle/Stat]
- Added to check engine status and gps data
This commit is contained in:
Hyojin Ahn 2025-12-24 16:16:39 -05:00
parent 26d05d35f0
commit 1a6ab4669d
10 changed files with 266 additions and 17 deletions

View File

@ -1,7 +1,7 @@
package com.goi.integration.samsara.client; package com.goi.integration.samsara.client;
import com.goi.integration.common.dto.ExtIngestResult; import com.goi.integration.common.dto.ExtIngestResult;
import com.goi.integration.samsara.dto.ExtSamsaraInspectionIngestCommand; import com.goi.integration.samsara.dto.ExtInspectionIngestCommand;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -27,7 +27,7 @@ public class OprIngestClient {
// @Autowired // @Autowired
// private ObjectMapper objectMapper; // private ObjectMapper objectMapper;
public ExtIngestResult ingestInspection(ExtSamsaraInspectionIngestCommand command) { public ExtIngestResult ingestInspection(ExtInspectionIngestCommand command) {
// String json = objectMapper // String json = objectMapper
// .writerWithDefaultPrettyPrinter() // .writerWithDefaultPrettyPrinter()
// .writeValueAsString(command); // .writeValueAsString(command);

View File

@ -6,6 +6,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import java.time.Instant; import java.time.Instant;
import java.util.List;
@Component @Component
public class SamsaraClient { public class SamsaraClient {
@ -38,4 +39,23 @@ public class SamsaraClient {
.bodyToMono(String.class) .bodyToMono(String.class)
.block(); .block();
} }
/**
* Vehicle live stats (engine + gps)
* Raw JSON 반환
*/
public String getVehicleStatsFeed(List<String> vehicleExternalIds) {
String vehicleIdsParam = String.join(",", vehicleExternalIds);
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/fleet/vehicles/stats/feed")
.queryParam("vehicleIds", vehicleIdsParam)
.queryParam("types", "engineStates,gps") // 나중에 필요하면 추가.
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}
} }

View File

@ -1,6 +1,6 @@
package com.goi.integration.samsara.controller; package com.goi.integration.samsara.controller;
import com.goi.integration.samsara.job.SamsaraDvirIngestJob; import com.goi.integration.samsara.job.DvirIngestJob;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor @RequiredArgsConstructor
public class SamsaraTestController { public class SamsaraTestController {
private final SamsaraDvirIngestJob dvirJob; private final DvirIngestJob dvirJob;
@PostMapping("/samsara-dvir") @PostMapping("/samsara-dvir")
public String runDvirNow() { public String runDvirNow() {

View File

@ -0,0 +1,25 @@
package com.goi.integration.samsara.controller;
import com.goi.integration.samsara.dto.VehicleStatResponseDto;
import com.goi.integration.samsara.service.VehicleStatService;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/vehicle")
@RequiredArgsConstructor
public class VehicleController {
private final VehicleStatService vehicleStatService;
@GetMapping("/stat")
public List<VehicleStatResponseDto> getStats(
@RequestParam List<String> vehicleIds
) {
return vehicleStatService.getVehicleStats(vehicleIds);
}
}

View File

@ -12,9 +12,9 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class ExtSamsaraInspectionIngestCommand { public class ExtInspectionIngestCommand {
private String source; private String source;
private String recordType; private String recordType;
private LocalDateTime fetchedAt; private LocalDateTime fetchedAt;
private List<ExtSamsaraInspectionRecordDto> records; private List<ExtInspectionRecordDto> records;
} }

View File

@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
public class ExtSamsaraInspectionRecordDto { public class ExtInspectionRecordDto {
private String externalId; // inspection id private String externalId; // inspection id
private String vehicleExternalId; // vehicle.id private String vehicleExternalId; // vehicle.id
private String driverExternalId; // signatoryUser.id private String driverExternalId; // signatoryUser.id

View File

@ -0,0 +1,21 @@
package com.goi.integration.samsara.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 VehicleStatResponseDto {
String vesVehicleExternalId;
Boolean vesEngineOn;
BigDecimal vesLatitude;
BigDecimal vesLongitude;
Instant vesGpsTime; // UTC 기준 절대 시점
}

View File

@ -4,7 +4,7 @@ import com.goi.integration.common.config.ScheduleJobConfigDto;
import com.goi.integration.common.config.ScheduleJobConfigProvider; import com.goi.integration.common.config.ScheduleJobConfigProvider;
import com.goi.integration.common.dto.ExtIngestResult; import com.goi.integration.common.dto.ExtIngestResult;
import com.goi.integration.samsara.client.SamsaraClient; import com.goi.integration.samsara.client.SamsaraClient;
import com.goi.integration.samsara.service.SamsaraInspectionIngestService; import com.goi.integration.samsara.service.InspectionIngestService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -17,13 +17,13 @@ import java.time.Instant;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class SamsaraDvirIngestJob { public class DvirIngestJob {
private static final String JOB_CODE = "SAMSARA_DVIR"; private static final String JOB_CODE = "SAMSARA_DVIR";
private final ScheduleJobConfigProvider configProvider; private final ScheduleJobConfigProvider configProvider;
private final SamsaraClient samsaraClient; private final SamsaraClient samsaraClient;
private final SamsaraInspectionIngestService ingestService; private final InspectionIngestService ingestService;
@Scheduled(cron = "${ext.samsara.jobs.dvir.cron:0 */10 * * * *}") @Scheduled(cron = "${ext.samsara.jobs.dvir.cron:0 */10 * * * *}")
public void run() { public void run() {

View File

@ -6,8 +6,8 @@ import com.goi.integration.common.dto.ExtIngestResult;
import com.goi.integration.common.util.DateTimeUtil; import com.goi.integration.common.util.DateTimeUtil;
import com.goi.integration.common.util.ExtPayloadHashUtil; import com.goi.integration.common.util.ExtPayloadHashUtil;
import com.goi.integration.samsara.client.OprIngestClient; import com.goi.integration.samsara.client.OprIngestClient;
import com.goi.integration.samsara.dto.ExtSamsaraInspectionIngestCommand; import com.goi.integration.samsara.dto.ExtInspectionIngestCommand;
import com.goi.integration.samsara.dto.ExtSamsaraInspectionRecordDto; import com.goi.integration.samsara.dto.ExtInspectionRecordDto;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -19,7 +19,7 @@ import java.util.List;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SamsaraInspectionIngestService { public class InspectionIngestService {
private static final String JOB_CODE = "SAMSARA_DVIR"; private static final String JOB_CODE = "SAMSARA_DVIR";
@ -46,7 +46,7 @@ public class SamsaraInspectionIngestService {
} }
// //
List<ExtSamsaraInspectionRecordDto> records = new ArrayList<>(); List<ExtInspectionRecordDto> records = new ArrayList<>();
log.info("[{}] data={}", JOB_CODE, data.size()); log.info("[{}] data={}", JOB_CODE, data.size());
// data[] node 하나의 inspection (preTrip / postTrip) // data[] node 하나의 inspection (preTrip / postTrip)
@ -69,7 +69,7 @@ public class SamsaraInspectionIngestService {
// record DTO // record DTO
records.add( records.add(
ExtSamsaraInspectionRecordDto.builder() ExtInspectionRecordDto.builder()
.externalId(externalId) .externalId(externalId)
.vehicleExternalId(vehicleExtId) .vehicleExternalId(vehicleExtId)
.driverExternalId(driverExtId) .driverExternalId(driverExtId)
@ -84,8 +84,8 @@ public class SamsaraInspectionIngestService {
} }
// ingest command 생성 // ingest command 생성
ExtSamsaraInspectionIngestCommand command = ExtInspectionIngestCommand command =
ExtSamsaraInspectionIngestCommand.builder() ExtInspectionIngestCommand.builder()
.source("SAMSARA") .source("SAMSARA")
.recordType("DVIR") .recordType("DVIR")
.fetchedAt(LocalDateTime.now()) .fetchedAt(LocalDateTime.now())

View File

@ -0,0 +1,183 @@
package com.goi.integration.samsara.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goi.integration.samsara.client.SamsaraClient;
import com.goi.integration.samsara.dto.VehicleStatResponseDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class VehicleStatService {
private final SamsaraClient samsaraClient;
private final ObjectMapper objectMapper;
/**
* 차량들의 실시간 상태 (engine + gps)
*/
public List<VehicleStatResponseDto> getVehicleStats(List<String> vehicleExternalIds) {
if (vehicleExternalIds == null || vehicleExternalIds.isEmpty()) {
return List.of();
}
// call Samsara API
String rawJson = samsaraClient.getVehicleStatsFeed(vehicleExternalIds);
if (rawJson == null || rawJson.isBlank()) {
log.warn("Samsara stats feed returned empty response");
return List.of();
}
// parsing response
try {
JsonNode root = objectMapper.readTree(rawJson);
JsonNode dataArray = root.path("data");
if (!dataArray.isArray()) {
log.warn("Unexpected Samsara response format: missing data[]");
return List.of();
}
// set response
List<VehicleStatResponseDto> result = new ArrayList<>();
for (JsonNode vehicleNode : dataArray) {
parseVehicleNode(vehicleNode).ifPresent(result::add);
}
return result;
} catch (Exception e) {
log.error("Failed to parse Samsara vehicle stats feed", e);
return List.of();
}
}
/* ===============================
Parsing 필요한 정보만
=============================== */
private Optional<VehicleStatResponseDto> parseVehicleNode(JsonNode vehicleNode) {
// Samsara vehicle ID (external)
String vehicleId = vehicleNode.path("id").asText(null);
if (vehicleId == null) {
return Optional.empty();
}
Boolean engineOn = extractLatestEngineState(vehicleNode.path("engineStates"));
GpsInfo gpsInfo = extractLatestGps(vehicleNode.path("gps"));
return Optional.of(
VehicleStatResponseDto.builder()
.vesVehicleExternalId(vehicleId)
.vesEngineOn(engineOn)
.vesLatitude(gpsInfo.latitude)
.vesLongitude(gpsInfo.longitude)
.vesGpsTime(gpsInfo.time)
.build()
);
}
/**
* 최신 engine 상태 추출
* @return true = On, false = Off, null = unknown
*/
private Boolean extractLatestEngineState(JsonNode engineStatesNode) {
if (!engineStatesNode.isArray() || engineStatesNode.isEmpty()) {
return null;
}
JsonNode latest = null;
Instant latestTime = null;
for (JsonNode node : engineStatesNode) {
Instant time = parseInstant(node.path("time").asText(null));
if (time != null && (latestTime == null || time.isAfter(latestTime))) {
latestTime = time;
latest = node;
}
}
if (latest == null) {
return null;
}
String value = latest.path("value").asText("");
return "On".equalsIgnoreCase(value);
}
/**
* 최신 GPS 정보 추출
*/
private GpsInfo extractLatestGps(JsonNode gpsArrayNode) {
if (!gpsArrayNode.isArray() || gpsArrayNode.isEmpty()) {
return new GpsInfo(null, null, null);
}
JsonNode latest = null;
Instant latestTime = null;
Iterator<JsonNode> it = gpsArrayNode.elements();
while (it.hasNext()) {
JsonNode node = it.next();
Instant time = parseInstant(node.path("time").asText(null));
if (time != null && (latestTime == null || time.isAfter(latestTime))) {
latestTime = time;
latest = node;
}
}
if (latest == null) {
return new GpsInfo(null, null, null);
}
BigDecimal lat = latest.hasNonNull("latitude")
? latest.get("latitude").decimalValue()
: null;
BigDecimal lon = latest.hasNonNull("longitude")
? latest.get("longitude").decimalValue()
: null;
return new GpsInfo(lat, lon, latestTime);
}
private Instant parseInstant(String value) {
try {
return value != null ? Instant.parse(value) : null;
} catch (Exception e) {
return null;
}
}
/* ===============================
Internal helper DTO
=============================== */
private static class GpsInfo {
BigDecimal latitude;
BigDecimal longitude;
Instant time;
GpsInfo(BigDecimal latitude, BigDecimal longitude, Instant time) {
this.latitude = latitude;
this.longitude = longitude;
this.time = time;
}
}
}