diff --git a/pom.xml b/pom.xml
index 81ba6a3..64c2842 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,10 +41,6 @@
-
- org.springframework.boot
- spring-boot-starter
-
org.springframework.boot
spring-boot-starter-web
@@ -106,6 +102,10 @@
${allure.version}
test
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
diff --git a/src/main/java/com/goi/integration/IntegrationServiceApplication.java b/src/main/java/com/goi/integration/IntegrationServiceApplication.java
new file mode 100644
index 0000000..290e8e1
--- /dev/null
+++ b/src/main/java/com/goi/integration/IntegrationServiceApplication.java
@@ -0,0 +1,11 @@
+package com.goi.integration;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.SpringApplication;
+
+@SpringBootApplication
+public class IntegrationServiceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(IntegrationServiceApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java b/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java
new file mode 100644
index 0000000..4cdedd1
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/config/InMemoryScheduleJobConfigProvider.java
@@ -0,0 +1,29 @@
+package com.goi.integration.common.config;
+
+import com.goi.integration.common.config.ScheduleJobConfigDto;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Optional;
+
+@Component
+public class InMemoryScheduleJobConfigProvider implements ScheduleJobConfigProvider {
+
+ private final Map configs = Map.of(
+ "SAMSARA_DVIR",
+ ScheduleJobConfigDto.builder()
+ .sjcJobCode("SAMSARA_DVIR")
+ .sjcEnabled(true)
+ .sjcCronExpression("0 */10 * * * *")
+ .sjcLookbackHours(24)
+ .sjcOverlapMinutes(10)
+ .sjcMaxRecords(252)
+ .sjcTimezone("UTC")
+ .build()
+ );
+
+ @Override
+ public Optional getJobConfig(String jobCode) {
+ return Optional.ofNullable(configs.get(jobCode));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/goi/integration/common/config/InternalWebClientFactory.java b/src/main/java/com/goi/integration/common/config/InternalWebClientFactory.java
new file mode 100644
index 0000000..52eda94
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/config/InternalWebClientFactory.java
@@ -0,0 +1,16 @@
+package com.goi.integration.common.config;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Component
+public class InternalWebClientFactory {
+
+ public WebClient create(String baseUrl, String token) {
+ return WebClient.builder()
+ .baseUrl(baseUrl)
+ .defaultHeader("X-INTERNAL-SERVICE", "integration-service")
+ .defaultHeader("X-INTERNAL-TOKEN", token)
+ .build();
+ }
+}
diff --git a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java
new file mode 100644
index 0000000..d88ab9c
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigDto.java
@@ -0,0 +1,20 @@
+package com.goi.integration.common.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ScheduleJobConfigDto {
+ private String sjcJobCode; // SAMSARA_DVIR
+ private String sjcCronExpression; // "0 */10 * * * *"
+ private Integer sjcLookbackHours; // 24
+ private Integer sjcOverlapMinutes;// 10
+ private Integer sjcMaxRecords; // 252
+ private Boolean sjcEnabled; // true
+ private String sjcTimezone; // "UTC"
+}
diff --git a/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java
new file mode 100644
index 0000000..692c00d
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/config/ScheduleJobConfigProvider.java
@@ -0,0 +1,10 @@
+package com.goi.integration.common.config;
+
+import com.goi.integration.common.config.ScheduleJobConfigDto;
+
+import java.util.Optional;
+
+public interface ScheduleJobConfigProvider {
+ Optional getJobConfig(String jobCode);
+}
+
diff --git a/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java b/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java
new file mode 100644
index 0000000..ece3a8b
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/dto/ExtIngestResult.java
@@ -0,0 +1,30 @@
+package com.goi.integration.common.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ExtIngestResult {
+ private String source;
+ private String recordType;
+ private int received;
+ private int inserted;
+ private int updated;
+ private int skipped;
+
+ public static ExtIngestResult empty(String source, String recordType) {
+ return ExtIngestResult.builder()
+ .source(source)
+ .recordType(recordType)
+ .received(0)
+ .inserted(0)
+ .updated(0)
+ .skipped(0)
+ .build();
+ }
+}
diff --git a/src/main/java/com/goi/integration/common/util/DateTimeUtil.java b/src/main/java/com/goi/integration/common/util/DateTimeUtil.java
new file mode 100644
index 0000000..001dce8
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/util/DateTimeUtil.java
@@ -0,0 +1,50 @@
+package com.goi.integration.common.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
+
+public final class DateTimeUtil {
+
+ private DateTimeUtil() {}
+
+ /**
+ * JsonNode → LocalDateTime
+ * (ISO-8601, Z / offset 지원)
+ */
+ public static LocalDateTime parse(JsonNode node) {
+ if (node == null || node.isMissingNode() || node.isNull()) {
+ return null;
+ }
+ return parse(node.asText(null));
+ }
+
+ /**
+ * String → LocalDateTime
+ * ex) 2025-12-22T13:30:24.365Z
+ * ex) 2025-12-22T13:30:24+00:00
+ */
+ public static LocalDateTime parse(String value) {
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+
+ try {
+ return OffsetDateTime.parse(value).toLocalDateTime();
+ } catch (DateTimeParseException e) {
+ return null; // ingest 안정성 우선
+ }
+ }
+
+ public static LocalDateTime parseToToronto(String value) {
+ if (value == null || value.isBlank()) return null;
+
+ return OffsetDateTime
+ .parse(value)
+ .atZoneSameInstant(ZoneId.of("America/Toronto"))
+ .toLocalDateTime();
+ }
+}
diff --git a/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java b/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java
new file mode 100644
index 0000000..8a63be2
--- /dev/null
+++ b/src/main/java/com/goi/integration/common/util/ExtPayloadHashUtil.java
@@ -0,0 +1,80 @@
+package com.goi.integration.common.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.TreeMap;
+
+public final class ExtPayloadHashUtil {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private ExtPayloadHashUtil() {}
+
+ /** record(JsonNode) → canonical hash */
+ public static String sha256FromNode(JsonNode node) {
+ try {
+ JsonNode normalized = normalize(node);
+ String canonicalJson = mapper.writeValueAsString(normalized);
+ return sha256(canonicalJson);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to hash payload", e);
+ }
+ }
+
+ /** raw JSON string → hash (fallback 용) */
+ public static String sha256FromJson(String rawJson) {
+ try {
+ JsonNode node = mapper.readTree(rawJson);
+ return sha256FromNode(node);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to hash raw json", e);
+ }
+ }
+
+ /* ---------- internal ---------- */
+
+ @SuppressWarnings({ "serial", "deprecation" })
+ private static JsonNode normalize(JsonNode node) {
+ if (node.isObject()) {
+ ObjectNode obj = mapper.createObjectNode();
+ new TreeMap() {{
+ node.fields().forEachRemaining(e -> put(e.getKey(), normalize(e.getValue())));
+ }}.forEach(obj::set);
+ return obj;
+ }
+ if (node.isArray()) {
+ ArrayNode arr = mapper.createArrayNode();
+ node.forEach(n -> arr.add(normalize(n)));
+ return arr;
+ }
+ return node;
+ }
+
+ private static String sha256(String value) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8));
+ StringBuilder hex = new StringBuilder();
+ for (byte b : hash) hex.append(String.format("%02x", b));
+ return hex.toString();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String sha256FromJsonNode(JsonNode node) {
+ try {
+ JsonNode normalized = normalize(node);
+ String canonicalJson = mapper.writeValueAsString(normalized);
+ return sha256(canonicalJson);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to hash JsonNode payload", e);
+ }
+ }
+
+}
diff --git a/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java
new file mode 100644
index 0000000..30880c1
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/client/OprIngestClient.java
@@ -0,0 +1,102 @@
+package com.goi.integration.samsara.client;
+
+import com.goi.integration.common.dto.ExtIngestResult;
+import com.goi.integration.samsara.dto.ExtSamsaraInspectionIngestCommand;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OprIngestClient {
+
+ private static final String INTERNAL_TOKEN_HEADER = "X-INTERNAL-TOKEN";
+
+ @Qualifier("oprWebClient")
+ private final WebClient oprWebClient;
+
+ @Value("${ext.opr.internal-token}")
+ private String token;
+
+// @Autowired
+// private ObjectMapper objectMapper;
+
+ public ExtIngestResult ingestInspection(ExtSamsaraInspectionIngestCommand command) {
+// String json = objectMapper
+// .writerWithDefaultPrettyPrinter()
+// .writeValueAsString(command);
+//
+ log.info(
+ "[OPR_INGEST][REQUEST] source={}, type={}, records={}, fetchedAt={}",
+ command.getSource(),
+ command.getRecordType(),
+ command.getRecords() != null ? command.getRecords().size() : 0,
+ command.getFetchedAt()
+ );
+
+ /*
+ try {
+ log.debug(
+ "[OPR_INGEST][REQUEST_BODY]\n{}",
+ new com.fasterxml.jackson.databind.ObjectMapper()
+ .writerWithDefaultPrettyPrinter()
+ .writeValueAsString(command)
+ );
+ } catch (Exception e) {
+ log.debug("[OPR_INGEST][REQUEST_BODY] failed to serialize", e);
+ }
+ */
+
+ try {
+ ExtIngestResult result = oprWebClient.post()
+ .uri("/ext/samsara/inspections/ingest")
+ .header(INTERNAL_TOKEN_HEADER, token)
+ .bodyValue(command)
+ .retrieve()
+ .onStatus(
+ status -> status.is4xxClientError() || status.is5xxServerError(),
+ resp -> resp.bodyToMono(String.class)
+ .map(body -> {
+ if (resp.statusCode().value() == 403) {
+ return new IllegalStateException(
+ "OPR_AUTH_FAILED: " + body
+ );
+ }
+ return new RuntimeException(
+ "OPR_INGEST_HTTP_ERROR: " + resp.statusCode() + " body=" + body
+ );
+ })
+ )
+ .bodyToMono(ExtIngestResult.class)
+ .block();
+
+ // 성공 로그
+ log.info(
+ "[OPR_INGEST][SUCCESS] received={}, inserted={}, updated={}, skipped={}",
+ result.getReceived(),
+ result.getInserted(),
+ result.getUpdated(),
+ result.getSkipped()
+ );
+
+ return result;
+
+ } catch (Exception e) {
+ // 실패 로그
+ log.error(
+ "[OPR_INGEST][FAILED] source={}, type={}, error={}",
+ command.getSource(),
+ command.getRecordType(),
+ e.getMessage(),
+ e
+ );
+ throw e;
+ }
+ }
+}
diff --git a/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java
new file mode 100644
index 0000000..fcfff35
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/client/SamsaraClient.java
@@ -0,0 +1,41 @@
+package com.goi.integration.samsara.client;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.time.Instant;
+
+@Component
+public class SamsaraClient {
+
+ private final WebClient webClient;
+
+ public SamsaraClient(
+ @Value("${ext.samsara.base-url:https://api.samsara.com}") String baseUrl,
+ @Value("${ext.samsara.api-token}") String apiToken
+ ) {
+ this.webClient = WebClient.builder()
+ .baseUrl(baseUrl)
+ .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiToken)
+ .build();
+ }
+
+ /**
+ * DVIR history raw JSON (string)로 받아도 되고, DTO로 매핑해도 됨.
+ * 일단 초기엔 String으로 받아서 JsonNode로 파싱하는게 가장 유연함.
+ */
+ public String getDvirHistory(int limit, Instant startTime, Instant endTime) {
+ return webClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/fleet/dvirs/history")
+ .queryParam("limit", limit)
+ .queryParam("startTime", startTime.toString())
+ .queryParam("endTime", endTime.toString())
+ .build())
+ .retrieve()
+ .bodyToMono(String.class)
+ .block();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/goi/integration/samsara/config/OprClientConfig.java b/src/main/java/com/goi/integration/samsara/config/OprClientConfig.java
new file mode 100644
index 0000000..b7197ce
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/config/OprClientConfig.java
@@ -0,0 +1,20 @@
+package com.goi.integration.samsara.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Configuration
+public class OprClientConfig {
+
+ @Bean
+ public WebClient oprWebClient(
+ WebClient.Builder builder,
+ @Value("${ext.opr.base-url}") String baseUrl
+ ) {
+ return builder
+ .baseUrl(baseUrl)
+ .build();
+ }
+}
diff --git a/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java b/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java
new file mode 100644
index 0000000..c83beb9
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/controller/SamsaraTestController.java
@@ -0,0 +1,19 @@
+package com.goi.integration.samsara.controller;
+
+import com.goi.integration.samsara.job.SamsaraDvirIngestJob;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/internal/test")
+@RequiredArgsConstructor
+public class SamsaraTestController {
+
+ private final SamsaraDvirIngestJob dvirJob;
+
+ @PostMapping("/samsara-dvir")
+ public String runDvirNow() {
+ dvirJob.run();
+ return "SAMSARA_DVIR_TRIGGERED";
+ }
+}
diff --git a/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java b/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java
new file mode 100644
index 0000000..6907606
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionIngestCommand.java
@@ -0,0 +1,20 @@
+package com.goi.integration.samsara.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ExtSamsaraInspectionIngestCommand {
+ private String source;
+ private String recordType;
+ private LocalDateTime fetchedAt;
+ 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/ExtSamsaraInspectionRecordDto.java
new file mode 100644
index 0000000..7358385
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/dto/ExtSamsaraInspectionRecordDto.java
@@ -0,0 +1,26 @@
+package com.goi.integration.samsara.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class ExtSamsaraInspectionRecordDto {
+ private String externalId; // inspection id
+ private String vehicleExternalId; // vehicle.id
+ private String driverExternalId; // signatoryUser.id
+ private String inspectionType; // preTrip / postTrip
+ private LocalDateTime startTime;
+ private LocalDateTime endTime;
+ private LocalDateTime signedAt;
+ private String payloadHash; // SHA-256
+ private JsonNode payloadJson; // raw inspection JSON
+}
diff --git a/src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java b/src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java
new file mode 100644
index 0000000..64f5674
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/job/SamsaraDvirIngestJob.java
@@ -0,0 +1,59 @@
+package com.goi.integration.samsara.job;
+
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.Instant;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SamsaraDvirIngestJob {
+
+ private static final String JOB_CODE = "SAMSARA_DVIR";
+
+ private final ScheduleJobConfigProvider configProvider;
+ private final SamsaraClient samsaraClient;
+ private final SamsaraInspectionIngestService ingestService;
+
+ @Scheduled(cron = "${ext.samsara.jobs.dvir.cron:0 */10 * * * *}")
+ public void run() {
+
+ ScheduleJobConfigDto cfg = configProvider.getJobConfig(JOB_CODE)
+ .filter(ScheduleJobConfigDto::getSjcEnabled)
+ .orElse(null);
+
+ if (cfg == null) {
+ log.info("[{}] disabled or config missing", JOB_CODE);
+ return;
+ }
+
+ int limit = (cfg.getSjcMaxRecords() != null) ? cfg.getSjcMaxRecords() : 252;
+
+ Instant now = Instant.now();
+ long lookbackSec = Duration.ofHours(cfg.getSjcLookbackHours() != null ? cfg.getSjcLookbackHours() : 24).getSeconds();
+ long overlapSec = Duration.ofMinutes(cfg.getSjcOverlapMinutes() != null ? cfg.getSjcOverlapMinutes() : 0).getSeconds();
+
+ Instant start = now.minusSeconds(lookbackSec + overlapSec);
+ Instant end = now;
+
+ log.info("[{}] calling samsara dvir history start={} end={} limit={}", JOB_CODE, start, end, limit);
+
+ String rawJson = samsaraClient.getDvirHistory(limit, start, end);
+
+ // opr-rest-api ingest 호출
+ ExtIngestResult result = ingestService.ingestFromRawJson(rawJson);
+
+ log.info("[{}] ingest result received={}, inserted={}, updated={}, skipped={}",
+ JOB_CODE, result.getReceived(), result.getInserted(), result.getUpdated(), result.getSkipped());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java b/src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java
new file mode 100644
index 0000000..220d0e9
--- /dev/null
+++ b/src/main/java/com/goi/integration/samsara/service/SamsaraInspectionIngestService.java
@@ -0,0 +1,103 @@
+package com.goi.integration.samsara.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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 lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SamsaraInspectionIngestService {
+
+ private static final String JOB_CODE = "SAMSARA_DVIR";
+
+ private final ObjectMapper objectMapper;
+ private final OprIngestClient oprIngestClient;
+
+ /**
+ * Samsara DVIR raw JSON 응답을 받아
+ * 1) record 단위로 분해
+ * 2) hash 생성
+ * 3) opr-rest-api ingest endpoint 호출
+ */
+ public ExtIngestResult ingestFromRawJson(String rawJson) {
+
+ try {
+ // 전체 JSON 파싱
+ JsonNode root = objectMapper.readTree(rawJson);
+ JsonNode data = root.path("data");
+
+ // data[] 없음
+ if (!data.isArray()) {
+ log.warn("Samsara DVIR response has no data[] array");
+ return ExtIngestResult.empty("SAMSARA", "DVIR");
+ }
+
+ //
+ List records = new ArrayList<>();
+ log.info("[{}] data={}", JOB_CODE, data.size());
+
+ // data[] 각 node 가 하나의 inspection (preTrip / postTrip)
+ for (JsonNode node : data) {
+ // parsing
+ String externalId = node.path("id").asText(null);
+ String vehicleExtId = node.path("vehicle").path("id").asText(null);
+ String driverExtId = node.path("authorSignature")
+ .path("signatoryUser")
+ .path("id")
+ .asText(null);
+ String inspectionType = node.path("type").asText(null);
+ LocalDateTime startTime = DateTimeUtil.parseToToronto(node.path("startTime").asText(null));
+ LocalDateTime endTime = DateTimeUtil.parseToToronto(node.path("endTime").asText(null));
+ LocalDateTime signedAt = DateTimeUtil.parseToToronto(
+ node.path("authorSignature").path("signedAtTime").asText(null)
+ );
+ // record 단위 hash 생성 (opr-rest-api 에서 idempotent ingest 판단용)
+ String hash = ExtPayloadHashUtil.sha256FromJsonNode(node);
+
+ // record DTO
+ records.add(
+ ExtSamsaraInspectionRecordDto.builder()
+ .externalId(externalId)
+ .vehicleExternalId(vehicleExtId)
+ .driverExternalId(driverExtId)
+ .inspectionType(inspectionType)
+ .startTime(startTime)
+ .endTime(endTime)
+ .signedAt(signedAt)
+ .payloadJson(node) // 원본 payload (JSON 그대로 저장)
+ .payloadHash(hash)
+ .build()
+ );
+ }
+
+ // ingest command 생성
+ ExtSamsaraInspectionIngestCommand command =
+ ExtSamsaraInspectionIngestCommand.builder()
+ .source("SAMSARA")
+ .recordType("DVIR")
+ .fetchedAt(LocalDateTime.now())
+ .records(records)
+ .build();
+
+ // opr-rest-api ingest endpoint 호출
+ return oprIngestClient.ingestInspection(command);
+
+ } catch (Exception e) {
+ log.error("Failed to ingest samsara DVIR payload", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 9cf1edd..97acab4 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -14,5 +14,39 @@ management:
exposure:
include: health
server:
+ port: 8091
servlet:
- context-path: /integration-service
\ No newline at end of file
+ context-path: /integration-service
+# ============================
+# External Integrations
+# ============================
+ext:
+ opr:
+ base-url: http://localhost:8083/opr-rest-api # opr-rest-api 주소
+ ingest-path: /ext/samsara/inspections/ingest
+ internal-token: ${OPR_INTERNAL_TOKEN}
+ hcm:
+ base-url: http://localhost:8081/hcm-rest-api
+ internal-token: ${HCM_INTERNAL_TOKEN}
+ acc:
+ base-url: http://localhost:8084/acc-rest-api
+ internal-token: ${ACC_INTERNAL_TOKEN}
+ samsara:
+ base-url: https://api.samsara.com
+ api-token: ${SAMSARA_API_TOKEN} # 반드시 env 로
+ timeout:
+ connect-ms: 5000
+ read-ms: 10000
+
+ jobs:
+ dvir:
+ cron: "0 */10 * * * *" # 10분
+# ============================
+# logging
+# ============================
+logging:
+ file:
+ name: logs/integration-service.log
+ level:
+ root: INFO
+ com.goi.integration: INFO
\ No newline at end of file