diff --git a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java index 30880c1..b290437 100644 --- a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java +++ b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java @@ -1,7 +1,7 @@ package com.goi.integration.samsara.client; 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.extern.slf4j.Slf4j; @@ -27,7 +27,7 @@ public class OprIngestClient { // @Autowired // private ObjectMapper objectMapper; - public ExtIngestResult ingestInspection(ExtSamsaraInspectionIngestCommand command) { + public ExtIngestResult ingestInspection(ExtInspectionIngestCommand command) { // String json = objectMapper // .writerWithDefaultPrettyPrinter() // .writeValueAsString(command); diff --git a/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java index fcfff35..98be3ee 100644 --- a/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java +++ b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import java.time.Instant; +import java.util.List; @Component public class SamsaraClient { @@ -38,4 +39,23 @@ public class SamsaraClient { .bodyToMono(String.class) .block(); } + + /** + * Vehicle live stats (engine + gps) + * Raw JSON 반환 + */ + public String getVehicleStatsFeed(List 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(); + } } \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java b/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java index c83beb9..820d96e 100644 --- a/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java +++ b/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java @@ -1,6 +1,6 @@ package com.goi.integration.samsara.controller; -import com.goi.integration.samsara.job.SamsaraDvirIngestJob; +import com.goi.integration.samsara.job.DvirIngestJob; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*; @RequiredArgsConstructor public class SamsaraTestController { - private final SamsaraDvirIngestJob dvirJob; + private final DvirIngestJob dvirJob; @PostMapping("/samsara-dvir") public String runDvirNow() { diff --git a/src/main/java/com/goi/integration/samsara/controller/VehicleController.java b/src/main/java/com/goi/integration/samsara/controller/VehicleController.java new file mode 100644 index 0000000..5c69cf4 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/controller/VehicleController.java @@ -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 getStats( + @RequestParam List vehicleIds + ) { + return vehicleStatService.getVehicleStats(vehicleIds); + } +} diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java similarity index 76% rename from src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java rename to src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java index 6907606..5d738fa 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java +++ b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionIngestCommand.java @@ -12,9 +12,9 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder -public class ExtSamsaraInspectionIngestCommand { +public class ExtInspectionIngestCommand { private String source; private String recordType; private LocalDateTime fetchedAt; - private List records; + private List records; } \ No newline at end of file diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionRecordDto.java b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java similarity index 94% rename from src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionRecordDto.java rename to src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java index 7358385..1eccb62 100644 --- a/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionRecordDto.java +++ b/src/main/java/com/goi/integration/samsara/dto/ExtInspectionRecordDto.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode; @NoArgsConstructor @AllArgsConstructor @Builder -public class ExtSamsaraInspectionRecordDto { +public class ExtInspectionRecordDto { private String externalId; // inspection id private String vehicleExternalId; // vehicle.id private String driverExternalId; // signatoryUser.id diff --git a/src/main/java/com/goi/integration/samsara/dto/VehicleStatResponseDto.java b/src/main/java/com/goi/integration/samsara/dto/VehicleStatResponseDto.java new file mode 100644 index 0000000..286ecc7 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/dto/VehicleStatResponseDto.java @@ -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 기준 절대 시점 +} diff --git a/src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java b/src/main/java/com/goi/integration/samsara/job/DvirIngestJob.java similarity index 92% rename from src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java rename to src/main/java/com/goi/integration/samsara/job/DvirIngestJob.java index 64f5674..b7ba393 100644 --- a/src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java +++ b/src/main/java/com/goi/integration/samsara/job/DvirIngestJob.java @@ -4,7 +4,7 @@ import com.goi.integration.common.config.ScheduleJobConfigDto; import com.goi.integration.common.config.ScheduleJobConfigProvider; import com.goi.integration.common.dto.ExtIngestResult; 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.extern.slf4j.Slf4j; @@ -17,13 +17,13 @@ import java.time.Instant; @Slf4j @Component @RequiredArgsConstructor -public class SamsaraDvirIngestJob { +public class DvirIngestJob { private static final String JOB_CODE = "SAMSARA_DVIR"; private final ScheduleJobConfigProvider configProvider; private final SamsaraClient samsaraClient; - private final SamsaraInspectionIngestService ingestService; + private final InspectionIngestService ingestService; @Scheduled(cron = "${ext.samsara.jobs.dvir.cron:0 */10 * * * *}") public void run() { diff --git a/src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java b/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java similarity index 89% rename from src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java rename to src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java index 220d0e9..cf6759e 100644 --- a/src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java +++ b/src/main/java/com/goi/integration/samsara/service/InspectionIngestService.java @@ -6,8 +6,8 @@ import com.goi.integration.common.dto.ExtIngestResult; import com.goi.integration.common.util.DateTimeUtil; import com.goi.integration.common.util.ExtPayloadHashUtil; import com.goi.integration.samsara.client.OprIngestClient; -import com.goi.integration.samsara.dto.ExtSamsaraInspectionIngestCommand; -import com.goi.integration.samsara.dto.ExtSamsaraInspectionRecordDto; +import com.goi.integration.samsara.dto.ExtInspectionIngestCommand; +import com.goi.integration.samsara.dto.ExtInspectionRecordDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -19,7 +19,7 @@ import java.util.List; @Slf4j @Service @RequiredArgsConstructor -public class SamsaraInspectionIngestService { +public class InspectionIngestService { private static final String JOB_CODE = "SAMSARA_DVIR"; @@ -46,7 +46,7 @@ public class SamsaraInspectionIngestService { } // - List records = new ArrayList<>(); + List records = new ArrayList<>(); log.info("[{}] data={}", JOB_CODE, data.size()); // data[] 각 node 가 하나의 inspection (preTrip / postTrip) @@ -69,7 +69,7 @@ public class SamsaraInspectionIngestService { // record DTO records.add( - ExtSamsaraInspectionRecordDto.builder() + ExtInspectionRecordDto.builder() .externalId(externalId) .vehicleExternalId(vehicleExtId) .driverExternalId(driverExtId) @@ -84,8 +84,8 @@ public class SamsaraInspectionIngestService { } // ingest command 생성 - ExtSamsaraInspectionIngestCommand command = - ExtSamsaraInspectionIngestCommand.builder() + ExtInspectionIngestCommand command = + ExtInspectionIngestCommand.builder() .source("SAMSARA") .recordType("DVIR") .fetchedAt(LocalDateTime.now()) diff --git a/src/main/java/com/goi/integration/samsara/service/VehicleStatService.java b/src/main/java/com/goi/integration/samsara/service/VehicleStatService.java new file mode 100644 index 0000000..403af16 --- /dev/null +++ b/src/main/java/com/goi/integration/samsara/service/VehicleStatService.java @@ -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 getVehicleStats(List 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 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 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 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; + } + } +}