[INSPECTION]
- Insert inspection raw data by integration-service ingest - Parse inspection data from raw data - Insert defects by parsing inspection raw [DISPATCH] - Open vehicle dispatch from preTrip inspection - Close vehicle dispatch from postTrip inspection
This commit is contained in:
parent
5a22c38ee7
commit
3347de524f
5
pom.xml
5
pom.xml
|
|
@ -75,6 +75,11 @@
|
|||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladmihalcea</groupId>
|
||||
<artifactId>hibernate-types-60</artifactId>
|
||||
<version>2.21.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>template</groupId>
|
||||
<artifactId>layered-architecture-template</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.goi.erp.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.goi.erp.config;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class InternalAuthFilter extends OncePerRequestFilter {
|
||||
|
||||
@Value("${internal.integration.token}")
|
||||
private String expectedToken;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String token = request.getHeader("X-INTERNAL-TOKEN");
|
||||
|
||||
if (request.getRequestURI().startsWith("/ext/")) {
|
||||
if (token == null || !token.equals(expectedToken)) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ public class SecurityConfig {
|
|||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용 안함
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/ext/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
) // 요청 권한 설정
|
||||
.addFilterBefore(new CorsFilter(corsConfigurationSource()), UsernamePasswordAuthenticationFilter.class) // JWT 필터 전에 CorsFilter 등록
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.goi.erp.controller;
|
||||
|
||||
import com.goi.erp.service.ExtSamsaraInspectionProcessor;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/test")
|
||||
@RequiredArgsConstructor
|
||||
public class TestController {
|
||||
|
||||
private final ExtSamsaraInspectionProcessor processor;
|
||||
|
||||
@PostMapping("/process-samsara-inspection")
|
||||
public String process() {
|
||||
processor.processUnprocessed();
|
||||
return "OK";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
package com.goi.erp.controller;
|
||||
|
||||
import com.goi.erp.common.permission.PermissionSet;
|
||||
import com.goi.erp.dto.VehicleDispatchRequestDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.common.permission.PermissionChecker;
|
||||
import com.goi.erp.token.PermissionAuthenticationToken;
|
||||
import com.goi.erp.service.VehicleDispatchService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/vehicle-dispatch")
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleDispatchController {
|
||||
|
||||
private final VehicleDispatchService dispatchService;
|
||||
private final VehicleDispatchRepository dispatchRepository;
|
||||
|
||||
// 기본값
|
||||
private final int defaultPage = 0;
|
||||
private final int defaultSize = 50;
|
||||
private final int maxSize = 500;
|
||||
|
||||
/* ============================================================
|
||||
CREATE (MANUAL)
|
||||
============================================================ */
|
||||
@PostMapping("/manual")
|
||||
public ResponseEntity<VehicleDispatch> createManualDispatch(
|
||||
@RequestBody VehicleDispatchRequestDto requestDto
|
||||
) {
|
||||
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.canCreateOPR(permissionSet)) {
|
||||
throw new AccessDeniedException("You do not have permission to create dispatch");
|
||||
}
|
||||
|
||||
VehicleDispatch created =
|
||||
dispatchService.createManualDispatch(requestDto, auth.getName());
|
||||
|
||||
return new ResponseEntity<>(created, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
PATCH (MANUAL)
|
||||
============================================================ */
|
||||
@PatchMapping("/manual/{uuid}")
|
||||
public ResponseEntity<VehicleDispatch> patchManualDispatch(
|
||||
@PathVariable UUID uuid,
|
||||
@RequestBody VehicleDispatchRequestDto requestDto
|
||||
) {
|
||||
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 update dispatch");
|
||||
}
|
||||
|
||||
VehicleDispatch updated =
|
||||
dispatchService.patchManualDispatch(uuid, requestDto, auth.getName());
|
||||
|
||||
return ResponseEntity.ok(updated);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
STATE CHANGE (MANUAL)
|
||||
============================================================ */
|
||||
@PostMapping("/{uuid}/pause")
|
||||
public ResponseEntity<Void> pauseDispatch(
|
||||
@PathVariable UUID uuid,
|
||||
@RequestParam(required = false) LocalDateTime pausedAt
|
||||
) {
|
||||
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 update dispatch");
|
||||
}
|
||||
|
||||
dispatchService.pauseDispatch(uuid, pausedAt);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{uuid}/close")
|
||||
public ResponseEntity<Void> closeDispatch(
|
||||
@PathVariable UUID uuid,
|
||||
@RequestParam(required = false) LocalDateTime endAt,
|
||||
@RequestParam(required = false) BigDecimal endOdometerEnd, // DB: ved_odometer_end 로 저장
|
||||
@RequestParam(required = false) String reason
|
||||
) {
|
||||
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 update dispatch");
|
||||
}
|
||||
|
||||
dispatchService.closeDispatch(
|
||||
uuid,
|
||||
endAt,
|
||||
endOdometerEnd,
|
||||
(reason == null || reason.isBlank()) ? "MANUAL_CLOSE" : reason
|
||||
);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
READ ONE
|
||||
============================================================ */
|
||||
@GetMapping("/{uuid}")
|
||||
public ResponseEntity<VehicleDispatch> getOne(
|
||||
@PathVariable UUID uuid
|
||||
) {
|
||||
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.canReadOPRAll(permissionSet)) {
|
||||
throw new AccessDeniedException("You do not have permission to read dispatch data");
|
||||
}
|
||||
|
||||
return dispatchRepository.findByVedUuid(uuid)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
READ ALL (Paged)
|
||||
============================================================ */
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<VehicleDispatch>> getAll(
|
||||
@RequestParam(required = false) Integer page,
|
||||
@RequestParam(required = false) Integer size
|
||||
) {
|
||||
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.canReadOPRAll(permissionSet)) {
|
||||
throw new AccessDeniedException("You do not have permission to read dispatch data");
|
||||
}
|
||||
|
||||
int p = (page == null) ? defaultPage : page;
|
||||
int s = (size == null) ? defaultSize : size;
|
||||
if (s > maxSize) s = maxSize;
|
||||
|
||||
Pageable pageable = PageRequest.of(p, s);
|
||||
return ResponseEntity.ok(dispatchRepository.findAll(pageable));
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
READ BY DATE (가장 자주 쓰는 API)
|
||||
- date만: 그날 전체
|
||||
- date + vehId: 그 차량의 그날
|
||||
- date + driverId: 그 기사 그날
|
||||
============================================================ */
|
||||
@GetMapping("/by-date")
|
||||
public ResponseEntity<List<VehicleDispatch>> getByDate(
|
||||
@RequestParam LocalDate date,
|
||||
@RequestParam(required = false) Long vehId,
|
||||
@RequestParam(required = false) Long driverId
|
||||
) {
|
||||
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.canReadOPRAll(permissionSet)) {
|
||||
throw new AccessDeniedException("You do not have permission to read dispatch data");
|
||||
}
|
||||
|
||||
if (vehId != null && driverId != null) {
|
||||
// 둘 다 넣는 케이스가 있을까
|
||||
throw new IllegalArgumentException("Use either vehId or driverId, not both");
|
||||
}
|
||||
|
||||
if (vehId != null) {
|
||||
return ResponseEntity.ok(
|
||||
dispatchRepository.findByVedVehIdAndVedDispatchDate(vehId, date)
|
||||
);
|
||||
}
|
||||
|
||||
if (driverId != null) {
|
||||
return ResponseEntity.ok(
|
||||
dispatchRepository.findByVedDriverIdAndVedDispatchDate(driverId, date)
|
||||
);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(
|
||||
dispatchRepository.findByVedDispatchDate(date)
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
JOB TRIGGER
|
||||
- paused to closed
|
||||
============================================================ */
|
||||
@PostMapping("/jobs/close-paused")
|
||||
public ResponseEntity<Void> closePaused(
|
||||
@RequestParam(defaultValue = "60") long minutes
|
||||
) {
|
||||
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 update dispatch");
|
||||
}
|
||||
|
||||
dispatchService.closePausedDispatches(Duration.ofMinutes(minutes));
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.goi.erp.controller;
|
||||
|
||||
import com.goi.erp.dto.VehicleExternalMapResponseDto;
|
||||
import com.goi.erp.service.VehicleExternalMapService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/employee")
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleExternalMapController {
|
||||
|
||||
private final VehicleExternalMapService externalMapService;
|
||||
|
||||
/**
|
||||
* 외부 시스템의 employeeId → 내부 employee 매핑 조회
|
||||
* 목적: customer_daily_order.driver_id 설정
|
||||
*
|
||||
* 예: GET /employee/external?solutionType=SAMSARA&externalId=28147...
|
||||
*/
|
||||
@GetMapping("/external")
|
||||
public ResponseEntity<VehicleExternalMapResponseDto> getVehicleMapping(
|
||||
@RequestParam String solutionType,
|
||||
@RequestParam String externalId
|
||||
) {
|
||||
VehicleExternalMapResponseDto dto =
|
||||
externalMapService.findMapping(solutionType, externalId);
|
||||
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.goi.erp.controller.ingest;
|
||||
|
||||
import com.goi.erp.dto.ExtApiErrorResponse;
|
||||
import com.goi.erp.dto.ExtIngestResult;
|
||||
import com.goi.erp.dto.ExtSamsaraInspectionIngestCommand;
|
||||
import com.goi.erp.service.ExtSamsaraInspectionIngestService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ext/samsara/inspections")
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraInspectionIngestController {
|
||||
|
||||
private final ExtSamsaraInspectionIngestService ingestService;
|
||||
|
||||
/**
|
||||
* Samsara DVIR raw ingest endpoint
|
||||
* Called by integration-service
|
||||
*/
|
||||
@PostMapping("/ingest")
|
||||
public ResponseEntity<?> ingest(
|
||||
@RequestBody ExtSamsaraInspectionIngestCommand command
|
||||
) {
|
||||
try {
|
||||
ExtIngestResult result = ingestService.ingest(command);
|
||||
return ResponseEntity.ok(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
log.error(
|
||||
"[SAMSARA_DVIR][INGEST_ERROR] records={}, error={}",
|
||||
command.getRecords() != null ? command.getRecords().size() : 0,
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(
|
||||
ExtApiErrorResponse.builder()
|
||||
.service("opr-rest-api")
|
||||
.errorCode("SAMSARA_DVIR_INGEST_FAILED")
|
||||
.message("Failed to ingest Samsara DVIR data")
|
||||
.detail(e.getMessage())
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ExtApiErrorResponse {
|
||||
private String service; // opr-rest-api
|
||||
private String errorCode; // 예: INGEST_FAILED
|
||||
private String message; // 사람이 읽는 메시지
|
||||
private String detail; // stack / 원인
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.goi.erp.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;
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.goi.erp.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<ExtSamsaraInspectionRecordDto> records;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.goi.erp.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;
|
||||
private String vehicleExternalId;
|
||||
private String driverExternalId;
|
||||
private String inspectionType;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private LocalDateTime signedAt;
|
||||
private String payloadHash;
|
||||
private JsonNode payloadJson;
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class VehicleDispatchRequestDto {
|
||||
|
||||
private Long vedVehId;
|
||||
private Long vedDriverId;
|
||||
private Long vedSubDriverId;
|
||||
private String vedExternalVehicleId; // SAMSARA vehicle id
|
||||
private String vedExternalDriverId;
|
||||
private LocalDate vedDispatchDate;
|
||||
private Integer vedShift;
|
||||
private String vedStatus;
|
||||
private LocalDateTime vedStartAt;
|
||||
private LocalDateTime vedPausedAt;
|
||||
private LocalDateTime vedEndAt;
|
||||
private String vedEndReason;
|
||||
private BigDecimal vedOdometerStart;
|
||||
private BigDecimal vedOdometerEnd;
|
||||
private BigDecimal vedOdometerIncrement;
|
||||
private String vedOdometerSource; // obd / gpsDistance / gpsOdometer
|
||||
private String vedSource; // AUTO / MANUAL
|
||||
private Integer vedEventCount;
|
||||
|
||||
// inspection 연계용 (AUTO 생성 시 사용)
|
||||
private Long vedInspectionId;
|
||||
private UUID vedInspectionUuid;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class VehicleDispatchResponseDto {
|
||||
|
||||
// 식별자
|
||||
private UUID vedUuid;
|
||||
|
||||
// 차량 정보 (View용)
|
||||
private String vehicleNo;
|
||||
private String vehicleName;
|
||||
private String licensePlate;
|
||||
|
||||
// 운전자 정보
|
||||
private String driverName;
|
||||
private String driverEmpNo;
|
||||
private String subDriverName;
|
||||
|
||||
// 배차 정보
|
||||
private String vedStatus;
|
||||
private LocalDate vedDispatchDate;
|
||||
private Integer vedShift;
|
||||
private LocalDateTime vedStartAt;
|
||||
private LocalDateTime vedPausedAt;
|
||||
private LocalDateTime vedEndAt;
|
||||
private String vedEndReason;
|
||||
|
||||
private BigDecimal vedOdometerStart;
|
||||
private BigDecimal vedOdometerEnd;
|
||||
private BigDecimal vedOdometerIncrement;
|
||||
private String vedOdometerSource;
|
||||
|
||||
private Integer vedEventCount;
|
||||
private String vedSource;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VehicleExternalMapRequestDto {
|
||||
private Long vexVehicleId; // 매핑할 내부 vehicle ID
|
||||
private String vexSolutionType; // 'SAMSARA', 'MIS', ...
|
||||
private String vexExternalId; // 외부 ID
|
||||
private String vexStatus; // A / I (옵션)
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
public class VehicleExternalMapResponseDto {
|
||||
|
||||
private UUID vexUuid;
|
||||
private Long vexVehicleId;
|
||||
|
||||
private String vexSolutionType;
|
||||
private String vexExternalId;
|
||||
|
||||
private String vexStatus;
|
||||
|
||||
private String vexCreatedBy;
|
||||
private String vexUpdatedBy;
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VehicleInspectionDefectDto {
|
||||
|
||||
private String defectType;
|
||||
private String comment;
|
||||
private Boolean isResolved;
|
||||
private Long resolvedBy;
|
||||
private String resolvedByExternalId;
|
||||
private LocalDateTime resolvedAt;
|
||||
private String vidExternalDefectId;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.goi.erp.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class VehicleInspectionDto {
|
||||
|
||||
private Long vehId;
|
||||
private String externalVehicleId;
|
||||
private UUID vehicleUuid;
|
||||
private Long driverId;
|
||||
private UUID driverUuid;
|
||||
private UUID subDriverUuid;
|
||||
private Long subDriverId;
|
||||
private String externalDriverId;
|
||||
private String externalSubDriverId;
|
||||
|
||||
private LocalDate inspectionDate;
|
||||
private String inspectionType; // preTrip / postTrip
|
||||
private LocalDateTime startAt;
|
||||
private LocalDateTime endAt;
|
||||
|
||||
private String result; // safe / unsafe
|
||||
private Long odometer;
|
||||
private Boolean hasDefect;
|
||||
|
||||
private String source; // SAMSARA / PAPER
|
||||
private Long sourceId;
|
||||
|
||||
private List<VehicleInspectionDefectDto> defects;
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "ext_samsara_raw_inspection",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_ext_samsara_raw_inspection_src_extid",
|
||||
columnNames = {"esri_source", "esri_external_id"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class ExtSamsaraRawInspection {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "esri_id")
|
||||
private Long esriId;
|
||||
|
||||
@Column(name = "esri_source", nullable = false, length = 20)
|
||||
private String esriSource; // SAMSARA
|
||||
|
||||
@Column(name = "esri_record_type", nullable = false, length = 20)
|
||||
private String esriRecordType; // DVIR
|
||||
|
||||
@Column(name = "esri_external_id", nullable = false, length = 50)
|
||||
private String esriExternalId; // inspection id
|
||||
|
||||
@Column(name = "esri_vehicle_ext_id", length = 50)
|
||||
private String esriVehicleExtId;
|
||||
|
||||
@Column(name = "esri_driver_ext_id", length = 50)
|
||||
private String esriDriverExtId;
|
||||
|
||||
@Column(name = "esri_start_time")
|
||||
private LocalDateTime esriStartTime;
|
||||
|
||||
@Column(name = "esri_end_time")
|
||||
private LocalDateTime esriEndTime;
|
||||
|
||||
@Column(name = "esri_signed_at")
|
||||
private LocalDateTime esriSignedAt;
|
||||
|
||||
@Column(name = "esri_inspection_type", length = 20)
|
||||
private String esriInspectionType; // preTrip / postTrip
|
||||
|
||||
@Column(name = "esri_hash", nullable = false, length = 64)
|
||||
private String esriHash;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name="esri_payload", columnDefinition = "jsonb", nullable = false)
|
||||
private JsonNode esriPayload;
|
||||
|
||||
@Column(name = "esri_fetched_at", nullable = false)
|
||||
private LocalDateTime esriFetchedAt;
|
||||
|
||||
@Column(
|
||||
name = "esri_fetched_date",
|
||||
insertable = false,
|
||||
updatable = false
|
||||
)
|
||||
private LocalDate esriFetchedDate;
|
||||
|
||||
@Column(name = "esri_processed")
|
||||
private Boolean esriProcessed;
|
||||
|
||||
@Column(name = "esri_processed_at")
|
||||
private LocalDateTime esriProcessedAt;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "vehicle_dispatch",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_vehicle_dispatch_shift",
|
||||
columnNames = {"ved_veh_id", "ved_dispatch_date", "ved_shift"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class VehicleDispatch {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "ved_id")
|
||||
private Long vedId;
|
||||
|
||||
@Column(name = "ved_uuid", unique = true)
|
||||
private UUID vedUuid;
|
||||
|
||||
@Column(name = "ved_status", length = 10)
|
||||
private String vedStatus; // O: Open | C: Closed | P: Paused
|
||||
|
||||
@Column(name = "ved_veh_id")
|
||||
private Long vedVehId;
|
||||
|
||||
@Column(name = "ved_driver_id")
|
||||
private Long vedDriverId;
|
||||
|
||||
@Column(name = "ved_sub_driver_id")
|
||||
private Long vedSubDriverId;
|
||||
|
||||
@Column(name = "ved_dispatch_date", nullable = false)
|
||||
private LocalDate vedDispatchDate;
|
||||
|
||||
@Column(name = "ved_shift")
|
||||
private Integer vedShift;
|
||||
|
||||
@Column(name = "ved_start_at")
|
||||
private LocalDateTime vedStartAt;
|
||||
|
||||
@Column(name = "ved_paused_at")
|
||||
private LocalDateTime vedPausedAt;
|
||||
|
||||
@Column(name = "ved_end_at")
|
||||
private LocalDateTime vedEndAt;
|
||||
|
||||
@Column(name = "ved_end_reason")
|
||||
private String vedEndReason;
|
||||
|
||||
@Column(name = "ved_odometer_start", precision = 12, scale = 2)
|
||||
private BigDecimal vedOdometerStart;
|
||||
|
||||
@Column(name = "ved_odometer_end", precision = 12, scale = 2)
|
||||
private BigDecimal vedOdometerEnd;
|
||||
|
||||
@Column(name = "ved_odometer_increment", precision = 12, scale = 2)
|
||||
private BigDecimal vedOdometerIncrement;
|
||||
|
||||
@Column(name = "ved_odometer_source", length = 20)
|
||||
private String vedOdometerSource; // obd / gpsDistance / gpsOdometer
|
||||
|
||||
@Column(name = "ved_event_count")
|
||||
private Integer vedEventCount;
|
||||
|
||||
@Column(name = "ved_source", length = 20)
|
||||
private String vedSource; // AUTO / MANUAL
|
||||
|
||||
@Column(name = "ved_created_at")
|
||||
private LocalDateTime vedCreatedAt;
|
||||
|
||||
@Column(name = "ved_created_by", length = 50)
|
||||
private String vedCreatedBy;
|
||||
|
||||
@Column(name = "ved_updated_at")
|
||||
private LocalDateTime vedUpdatedAt;
|
||||
|
||||
@Column(name = "ved_updated_by", length = 50)
|
||||
private String vedUpdatedBy;
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class VehicleExternalMap {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "vex_id")
|
||||
private Long vexId;
|
||||
|
||||
@Column(name = "vex_uuid", nullable = false)
|
||||
private UUID vexUuid;
|
||||
|
||||
@Column(name = "vex_vehicle_id", nullable = false)
|
||||
private Long vexVehicleId;
|
||||
|
||||
@Column(name = "vex_solution_type", nullable = false, length = 50)
|
||||
private String vexSolutionType;
|
||||
|
||||
@Column(name = "vex_external_id", nullable = false, length = 100)
|
||||
private String vexExternalId;
|
||||
|
||||
@Column(name = "vex_status", length = 1)
|
||||
private String vexStatus; // A / I
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "vex_created_at", updatable = false)
|
||||
private LocalDateTime vexCreatedAt;
|
||||
|
||||
@CreatedBy
|
||||
@Column(name = "vex_created_by")
|
||||
private String vexCreatedBy;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "vex_updated_at")
|
||||
private LocalDateTime vexUpdatedAt;
|
||||
|
||||
@LastModifiedBy
|
||||
@Column(name = "vex_updated_by")
|
||||
private String vexUpdatedBy;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "vehicle_inspection",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(
|
||||
name = "uk_vehicle_inspection_source",
|
||||
columnNames = {"vei_source", "vei_source_id"}
|
||||
)
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class VehicleInspection {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "vei_id")
|
||||
private Long veiId;
|
||||
|
||||
@Column(name = "vei_uuid", unique = true)
|
||||
private UUID veiUuid;
|
||||
|
||||
@Column(name = "vei_veh_id")
|
||||
private Long veiVehId;
|
||||
|
||||
@Column(name = "vei_driver_id")
|
||||
private Long veiDriverId;
|
||||
|
||||
@Column(name = "vei_sub_driver_id")
|
||||
private Long veiSubDriverId;
|
||||
|
||||
@Column(name = "vei_inspection_date", nullable = false)
|
||||
private LocalDate veiInspectionDate;
|
||||
|
||||
@Column(name = "vei_inspection_type", length = 20)
|
||||
private String veiInspectionType; // preTrip / postTrip
|
||||
|
||||
@Column(name = "vei_start_at")
|
||||
private LocalDateTime veiStartAt;
|
||||
|
||||
@Column(name = "vei_end_at")
|
||||
private LocalDateTime veiEndAt;
|
||||
|
||||
@Column(name = "vei_result", length = 20)
|
||||
private String veiResult; // safe / unsafe
|
||||
|
||||
@Column(name = "vei_odometer")
|
||||
private Long veiOdometer;
|
||||
|
||||
@Column(name = "vei_has_defect")
|
||||
private Boolean veiHasDefect;
|
||||
|
||||
@Column(name = "vei_source", length = 20)
|
||||
private String veiSource; // SAMSARA / PAPER
|
||||
|
||||
@Column(name = "vei_source_id")
|
||||
private Long veiSourceId; // external id or self id
|
||||
|
||||
@Column(name = "vei_created_at")
|
||||
private LocalDateTime veiCreatedAt;
|
||||
|
||||
@Column(name = "vei_dispatch_id")
|
||||
private Long veiDispatchId;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.goi.erp.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "vehicle_inspection_defect")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class VehicleInspectionDefect {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "vid_id")
|
||||
private Long vidId;
|
||||
|
||||
@Column(name = "vid_vei_id", nullable = false)
|
||||
private Long vidVeiId; // vehicle_inspection.vei_id
|
||||
|
||||
@Column(name = "vid_defect_type", length = 50)
|
||||
private String vidDefectType;
|
||||
|
||||
@Column(name = "vid_comment")
|
||||
private String vidComment;
|
||||
|
||||
@Column(name = "vid_external_defect_id")
|
||||
private String vidExternalDefectId;
|
||||
|
||||
@Column(name = "vid_is_resolved")
|
||||
private Boolean vidIsResolved;
|
||||
|
||||
@Column(name = "vid_resolved_by")
|
||||
private Long vidResolvedBy;
|
||||
|
||||
@Column(name = "vid_resolved_at")
|
||||
private LocalDateTime vidResolvedAt;
|
||||
|
||||
@Column(name = "vid_created_at")
|
||||
private LocalDateTime vidCreatedAt;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.ExtSamsaraRawInspection;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ExtSamsaraRawInspectionRepository
|
||||
extends JpaRepository<ExtSamsaraRawInspection, Long> {
|
||||
|
||||
/**
|
||||
* idempotent ingest 용
|
||||
* (source + externalId 는 UNIQUE)
|
||||
*/
|
||||
Optional<ExtSamsaraRawInspection> findByEsriSourceAndEsriExternalId(
|
||||
String esriSource,
|
||||
String esriExternalId
|
||||
);
|
||||
|
||||
/**
|
||||
* 아직 처리되지 않은 raw inspection 조회
|
||||
*/
|
||||
List<ExtSamsaraRawInspection> findByEsriProcessedFalse();
|
||||
|
||||
/**
|
||||
* 특정 날짜에 수집된 raw inspection 중 미처리 데이터
|
||||
* (scheduler / batch parsing 용)
|
||||
*/
|
||||
List<ExtSamsaraRawInspection> findByEsriProcessedFalseAndEsriFetchedDate(
|
||||
LocalDate esriFetchedDate
|
||||
);
|
||||
|
||||
/**
|
||||
* 특정 차량의 inspection 이력 조회 (디버깅/분석용)
|
||||
*/
|
||||
List<ExtSamsaraRawInspection> findByEsriVehicleExtId(
|
||||
String esriVehicleExtId
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface VehicleDispatchRepository extends JpaRepository<VehicleDispatch, Long> {
|
||||
|
||||
List<VehicleDispatch> findByVedVehIdAndVedDispatchDate(Long vedVehId, LocalDate vedDispatchDate);
|
||||
List<VehicleDispatch> findByVedDriverIdAndVedDispatchDate(Long vedDriverId, LocalDate vedDispatchDate);
|
||||
List<VehicleDispatch> findByVedDispatchDate(LocalDate vedDispatchDate);
|
||||
Page<VehicleDispatch> findAll(Pageable pageable);
|
||||
|
||||
Optional<VehicleDispatch> findByVedUuid(UUID vedUuid);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedVehId = :vehId
|
||||
AND d.vedStatus IN ('O','P')
|
||||
""")
|
||||
Optional<VehicleDispatch> findOpenDispatchByVehId(Long vehId);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT d
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedStatus = 'P'
|
||||
AND d.vedPausedAt <= :threshold
|
||||
""")
|
||||
List<VehicleDispatch> findPausedBefore(LocalDateTime threshold);
|
||||
|
||||
//
|
||||
@Query("""
|
||||
SELECT COALESCE(MAX(d.vedShift), -1)
|
||||
FROM VehicleDispatch d
|
||||
WHERE d.vedVehId = :vehId
|
||||
AND d.vedDispatchDate = :date
|
||||
""")
|
||||
Integer findMaxShift(Long vehId, LocalDate date);
|
||||
|
||||
//
|
||||
default Integer findNextShift(Long vehId, LocalDate date) {
|
||||
Integer max = findMaxShift(vehId, date);
|
||||
return max == null ? 0 : max + 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleExternalMap;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface VehicleExternalMapRepository extends JpaRepository<VehicleExternalMap, Long> {
|
||||
|
||||
Optional<VehicleExternalMap> findByVexSolutionTypeAndVexExternalIdAndVexStatus(
|
||||
String vexSolutionType,
|
||||
String vexExternalId,
|
||||
String vexStatus
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleInspectionDefect;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface VehicleInspectionDefectRepository extends JpaRepository<VehicleInspectionDefect, Long> {
|
||||
|
||||
List<VehicleInspectionDefect> findByVidVeiId(Long vidVeiId);
|
||||
List<VehicleInspectionDefect> findByVidVeiIdAndVidIsResolvedFalse(Long vidVeiId);
|
||||
|
||||
Optional<VehicleInspectionDefect> findByVidVeiIdAndVidExternalDefectId(Long veiId, String vidExternalDefectId);
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.goi.erp.repository;
|
||||
|
||||
import com.goi.erp.entity.VehicleInspection;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface VehicleInspectionRepository extends JpaRepository<VehicleInspection, Long> {
|
||||
|
||||
Optional<VehicleInspection> findByVeiSourceAndVeiSourceId(String veiSource, Long veiSourceId);
|
||||
Optional<VehicleInspection> findByVeiVehIdAndVeiInspectionDate(Long veiVehId, LocalDate veiInspectionDate);
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.ExtIngestResult;
|
||||
import com.goi.erp.dto.ExtSamsaraInspectionIngestCommand;
|
||||
import com.goi.erp.dto.ExtSamsaraInspectionRecordDto;
|
||||
import com.goi.erp.entity.ExtSamsaraRawInspection;
|
||||
import com.goi.erp.repository.ExtSamsaraRawInspectionRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraInspectionIngestService {
|
||||
|
||||
private final ExtSamsaraRawInspectionRepository repository;
|
||||
|
||||
/**
|
||||
* Ingest entry point
|
||||
*/
|
||||
@Transactional
|
||||
public ExtIngestResult ingest(ExtSamsaraInspectionIngestCommand command) {
|
||||
|
||||
int inserted = 0;
|
||||
int updated = 0;
|
||||
int skipped = 0;
|
||||
@SuppressWarnings("unused")
|
||||
int failed = 0;
|
||||
|
||||
for (ExtSamsaraInspectionRecordDto record : command.getRecords()) {
|
||||
|
||||
IngestAction action;
|
||||
|
||||
try {
|
||||
action = ingestSingle(command, record);
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case INSERTED -> inserted++;
|
||||
case UPDATED -> updated++;
|
||||
case SKIPPED -> skipped++;
|
||||
case FAILED -> failed++;
|
||||
}
|
||||
}
|
||||
|
||||
return ExtIngestResult.builder()
|
||||
.source(command.getSource())
|
||||
.recordType(command.getRecordType())
|
||||
.received(command.getRecords().size())
|
||||
.inserted(inserted)
|
||||
.updated(updated)
|
||||
.skipped(skipped)
|
||||
// .failed(failed) // 필요하면 추후 추가
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Single record ingest (idempotent)
|
||||
*/
|
||||
private IngestAction ingestSingle(
|
||||
ExtSamsaraInspectionIngestCommand command,
|
||||
ExtSamsaraInspectionRecordDto record
|
||||
) {
|
||||
|
||||
return repository
|
||||
.findByEsriSourceAndEsriExternalId(
|
||||
command.getSource(),
|
||||
record.getExternalId()
|
||||
)
|
||||
.map(existing -> updateIfChanged(existing, command, record))
|
||||
.orElseGet(() -> {
|
||||
insertNew(command, record);
|
||||
return IngestAction.INSERTED;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Insert new raw inspection
|
||||
*/
|
||||
private void insertNew(
|
||||
ExtSamsaraInspectionIngestCommand command,
|
||||
ExtSamsaraInspectionRecordDto record
|
||||
) {
|
||||
|
||||
ExtSamsaraRawInspection entity = ExtSamsaraRawInspection.builder()
|
||||
.esriSource(command.getSource())
|
||||
.esriRecordType(command.getRecordType())
|
||||
.esriExternalId(record.getExternalId())
|
||||
.esriVehicleExtId(record.getVehicleExternalId())
|
||||
.esriDriverExtId(record.getDriverExternalId())
|
||||
.esriInspectionType(record.getInspectionType())
|
||||
.esriStartTime(record.getStartTime())
|
||||
.esriEndTime(record.getEndTime())
|
||||
.esriSignedAt(record.getSignedAt())
|
||||
.esriHash(record.getPayloadHash())
|
||||
.esriFetchedAt(command.getFetchedAt())
|
||||
.esriProcessed(false)
|
||||
.esriPayload(record.getPayloadJson())
|
||||
.build();
|
||||
|
||||
repository.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update only if hash changed
|
||||
*/
|
||||
private IngestAction updateIfChanged(
|
||||
ExtSamsaraRawInspection existing,
|
||||
ExtSamsaraInspectionIngestCommand command,
|
||||
ExtSamsaraInspectionRecordDto record
|
||||
) {
|
||||
|
||||
if (existing.getEsriHash().equals(record.getPayloadHash())) {
|
||||
return IngestAction.SKIPPED;
|
||||
}
|
||||
|
||||
existing.setEsriVehicleExtId(record.getVehicleExternalId());
|
||||
existing.setEsriDriverExtId(record.getDriverExternalId());
|
||||
existing.setEsriInspectionType(record.getInspectionType());
|
||||
existing.setEsriStartTime(record.getStartTime());
|
||||
existing.setEsriEndTime(record.getEndTime());
|
||||
existing.setEsriSignedAt(record.getSignedAt());
|
||||
existing.setEsriHash(record.getPayloadHash());
|
||||
existing.setEsriFetchedAt(command.getFetchedAt());
|
||||
existing.setEsriPayload(record.getPayloadJson());
|
||||
existing.setEsriProcessed(false);
|
||||
existing.setEsriProcessedAt(null);
|
||||
|
||||
repository.save(existing);
|
||||
return IngestAction.UPDATED;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.goi.erp.dto.VehicleInspectionDefectDto;
|
||||
import com.goi.erp.dto.VehicleInspectionDto;
|
||||
import com.goi.erp.entity.ExtSamsaraRawInspection;
|
||||
import com.goi.erp.repository.ExtSamsaraRawInspectionRepository;
|
||||
import com.goi.erp.common.util.DateTimeUtil;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ExtSamsaraInspectionProcessor {
|
||||
|
||||
private final ExtSamsaraRawInspectionRepository rawRepo;
|
||||
private final VehicleInspectionService vehicleInspectionService;
|
||||
|
||||
@Transactional
|
||||
public void processUnprocessed() {
|
||||
|
||||
List<ExtSamsaraRawInspection> raws = rawRepo.findByEsriProcessedFalse();
|
||||
|
||||
if (raws.isEmpty()) {
|
||||
log.info("[DVIR_PROCESSOR] no unprocessed raw inspections");
|
||||
return;
|
||||
}
|
||||
// log 용
|
||||
int processed = 0;
|
||||
int failed = 0;
|
||||
|
||||
for (ExtSamsaraRawInspection raw : raws) {
|
||||
try {
|
||||
processSingle(raw);
|
||||
processed++;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
log.error(
|
||||
"[DVIR_PROCESSOR] failed rawId={} externalId={} msg={}",
|
||||
raw.getEsriId(),
|
||||
raw.getEsriExternalId(),
|
||||
e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[DVIR_PROCESSOR] done processed={} failed={}", processed, failed);
|
||||
}
|
||||
|
||||
private void processSingle(ExtSamsaraRawInspection raw) {
|
||||
|
||||
JsonNode payload = raw.getEsriPayload();
|
||||
|
||||
// Inspection
|
||||
VehicleInspectionDto dto = new VehicleInspectionDto();
|
||||
dto.setSource("SAMSARA");
|
||||
dto.setSourceId(Long.valueOf(raw.getEsriExternalId()));
|
||||
dto.setExternalVehicleId(raw.getEsriVehicleExtId());
|
||||
dto.setExternalDriverId(raw.getEsriDriverExtId());
|
||||
dto.setInspectionType(raw.getEsriInspectionType());
|
||||
|
||||
dto.setInspectionDate(
|
||||
raw.getEsriStartTime() != null
|
||||
? raw.getEsriStartTime().toLocalDate()
|
||||
: raw.getEsriFetchedAt().toLocalDate()
|
||||
);
|
||||
|
||||
dto.setStartAt(raw.getEsriStartTime());
|
||||
dto.setEndAt(raw.getEsriEndTime());
|
||||
dto.setResult(payload.path("safetyStatus").asText(null));
|
||||
dto.setOdometer(payload.path("odometerMeters").asLong(0L));
|
||||
|
||||
log.info(
|
||||
"[DVIR] rawId={} odometerMeters node={} value={}",
|
||||
raw.getEsriExternalId(),
|
||||
payload.get("odometerMeters"),
|
||||
payload.path("odometerMeters").asLong(-1)
|
||||
);
|
||||
|
||||
// Defects
|
||||
JsonNode defectsNode = payload.path("vehicleDefects");
|
||||
List<VehicleInspectionDefectDto> defectDtos = new ArrayList<>();
|
||||
|
||||
if (defectsNode.isArray()) {
|
||||
for (JsonNode d : defectsNode) {
|
||||
VehicleInspectionDefectDto defectDto = new VehicleInspectionDefectDto();
|
||||
defectDto.setVidExternalDefectId(d.path("id").asText(null));
|
||||
defectDto.setDefectType(d.path("defectType").asText(null));
|
||||
defectDto.setComment(d.path("comment").asText(null));
|
||||
defectDto.setIsResolved(d.path("isResolved").asBoolean(false));
|
||||
JsonNode resolvedBy = d.path("resolvedBy");
|
||||
if (!resolvedBy.isMissingNode()) {
|
||||
defectDto.setResolvedByExternalId(resolvedBy.path("id").asText(null));
|
||||
}
|
||||
defectDto.setResolvedAt(DateTimeUtil.parse(d.path("resolvedAtTime")));
|
||||
defectDto.setCreatedAt(DateTimeUtil.parse(d.path("createdAtTime")));
|
||||
defectDtos.add(defectDto);
|
||||
}
|
||||
}
|
||||
dto.setDefects(defectDtos);
|
||||
dto.setHasDefect(!defectDtos.isEmpty());
|
||||
|
||||
log.info(
|
||||
"[DVIR DTO] sourceId={} result={} odometer={} hasDefect={} defects={}",
|
||||
dto.getSourceId(),
|
||||
dto.getResult(),
|
||||
dto.getOdometer(),
|
||||
dto.getHasDefect(),
|
||||
dto.getDefects() != null ? dto.getDefects().size() : null
|
||||
);
|
||||
|
||||
// inspection + defect 저장은 Service 에서
|
||||
vehicleInspectionService.saveInspection(dto);
|
||||
|
||||
// raw 처리 완료
|
||||
raw.setEsriProcessed(true);
|
||||
raw.setEsriProcessedAt(LocalDateTime.now());
|
||||
rawRepo.save(raw);
|
||||
}
|
||||
}
|
||||
|
|
@ -25,9 +25,9 @@ public class HcmEmployeeClient {
|
|||
@Value("${hcm.api.base-url}")
|
||||
private String hcmBaseUrl;
|
||||
|
||||
public Long getEmpIdFromExternalId(String externalId) {
|
||||
public Long getEmpIdFromExternalId(String externalSource, String externalId) {
|
||||
|
||||
String url = hcmBaseUrl + "/employee/external" + "?solutionType=MIS&externalId=" + externalId;
|
||||
String url = hcmBaseUrl + "/employee/external" + "?solutionType=" + externalSource + "&externalId=" + externalId;
|
||||
|
||||
try {
|
||||
// set token in header
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
public enum IngestAction {
|
||||
INSERTED,
|
||||
UPDATED,
|
||||
SKIPPED,
|
||||
FAILED
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.VehicleDispatchRequestDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.entity.VehicleInspection;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
|
||||
import 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.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleDispatchService {
|
||||
|
||||
private final VehicleDispatchRepository dispatchRepository;
|
||||
|
||||
/* ===============================
|
||||
* CREATE
|
||||
* =============================== */
|
||||
public VehicleDispatch createFromPreTripInspection(
|
||||
Long vehId,
|
||||
Long driverId,
|
||||
Long subDriverId,
|
||||
LocalDate dispatchDate,
|
||||
LocalDateTime startAt,
|
||||
BigDecimal startOdometer,
|
||||
String source
|
||||
) {
|
||||
Integer nextShift = dispatchRepository.findNextShift(vehId, dispatchDate);
|
||||
|
||||
VehicleDispatch dispatch = VehicleDispatch.builder()
|
||||
.vedUuid(UUID.randomUUID())
|
||||
.vedVehId(vehId)
|
||||
.vedDriverId(driverId)
|
||||
.vedSubDriverId(subDriverId)
|
||||
.vedDispatchDate(dispatchDate)
|
||||
.vedShift(nextShift)
|
||||
.vedStartAt(startAt)
|
||||
.vedOdometerStart(startOdometer)
|
||||
.vedStatus("O")
|
||||
.vedSource(source != null ? source : "AUTO")
|
||||
.build();
|
||||
|
||||
return dispatchRepository.save(dispatch);
|
||||
}
|
||||
|
||||
public VehicleDispatch createManualDispatch(
|
||||
VehicleDispatchRequestDto dto,
|
||||
String createdBy
|
||||
) {
|
||||
VehicleDispatch opened =
|
||||
dispatchRepository.findOpenDispatchByVehId(dto.getVedVehId())
|
||||
.orElse(null);
|
||||
|
||||
if (opened != null) return opened;
|
||||
|
||||
Integer nextShift =
|
||||
dispatchRepository.findNextShift(dto.getVedVehId(), dto.getVedDispatchDate());
|
||||
|
||||
VehicleDispatch dispatch = VehicleDispatch.builder()
|
||||
.vedUuid(UUID.randomUUID())
|
||||
.vedVehId(dto.getVedVehId())
|
||||
.vedDriverId(dto.getVedDriverId())
|
||||
.vedSubDriverId(dto.getVedSubDriverId())
|
||||
.vedDispatchDate(dto.getVedDispatchDate())
|
||||
.vedShift(nextShift)
|
||||
.vedStartAt(dto.getVedStartAt() != null ? dto.getVedStartAt() : LocalDateTime.now())
|
||||
.vedOdometerStart(dto.getVedOdometerStart())
|
||||
.vedOdometerSource(dto.getVedOdometerSource())
|
||||
.vedStatus("O")
|
||||
.vedSource("MANUAL")
|
||||
.vedCreatedBy(createdBy)
|
||||
.build();
|
||||
|
||||
return dispatchRepository.save(dispatch);
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
* STATE TRANSITIONS
|
||||
* =============================== */
|
||||
public void pauseDispatch(UUID dispatchUuid, LocalDateTime pausedAt) {
|
||||
VehicleDispatch d = getOpen(dispatchUuid);
|
||||
|
||||
d.setVedStatus("P");
|
||||
d.setVedPausedAt(pausedAt != null ? pausedAt : LocalDateTime.now());
|
||||
|
||||
dispatchRepository.save(d);
|
||||
}
|
||||
|
||||
public void closeDispatch(
|
||||
UUID dispatchUuid,
|
||||
LocalDateTime endAt,
|
||||
BigDecimal endOdometer,
|
||||
String reason
|
||||
) {
|
||||
VehicleDispatch d = dispatchRepository.findByVedUuid(dispatchUuid)
|
||||
.orElseThrow(() -> new IllegalArgumentException("dispatch not found"));
|
||||
|
||||
if ("C".equals(d.getVedStatus())) {
|
||||
log.debug(
|
||||
"[DISPATCH][CLOSE] already closed. dispatchUuid={}",
|
||||
dispatchUuid
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
d.setVedStatus("C");
|
||||
d.setVedEndAt(endAt != null ? endAt : LocalDateTime.now());
|
||||
d.setVedEndReason(reason);
|
||||
|
||||
if (endOdometer != null) {
|
||||
d.setVedOdometerEnd(endOdometer);
|
||||
|
||||
if (d.getVedOdometerStart() != null) {
|
||||
d.setVedOdometerIncrement(
|
||||
endOdometer.subtract(d.getVedOdometerStart())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchRepository.save(d);
|
||||
}
|
||||
|
||||
|
||||
/* ===============================
|
||||
* INSPECTION 에 의한 배차 종료
|
||||
* =============================== */
|
||||
public void evaluateDispatchCloseByInspection(VehicleInspection inspection) {
|
||||
|
||||
VehicleDispatch open =
|
||||
dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId())
|
||||
.orElse(null);
|
||||
|
||||
if (open == null) {
|
||||
log.info(
|
||||
"[DISPATCH][EVAL] no open dispatch. inspectionType={} vehId={} driverId={}",
|
||||
inspection.getVeiInspectionType(),
|
||||
inspection.getVeiVehId(),
|
||||
inspection.getVeiDriverId()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rule 1: PostTrip
|
||||
if ("postTrip".equalsIgnoreCase(inspection.getVeiInspectionType())) {
|
||||
closeDispatch(
|
||||
open.getVedUuid(),
|
||||
inspection.getVeiEndAt(),
|
||||
inspection.getVeiOdometer() != null ? BigDecimal.valueOf(inspection.getVeiOdometer()): null,
|
||||
"POST_TRIP_INSPECTION"
|
||||
);
|
||||
log.info(
|
||||
"[DISPATCH][CLOSE] by POST_TRIP inspection. dispatchUuid={} endAt={}",
|
||||
open.getVedUuid(),
|
||||
inspection.getVeiEndAt()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rule 2: same vehicle, different driver, preTrip
|
||||
if ("preTrip".equalsIgnoreCase(inspection.getVeiInspectionType())
|
||||
&& !inspection.getVeiDriverId().equals(open.getVedDriverId())) {
|
||||
|
||||
closeDispatch(
|
||||
open.getVedUuid(),
|
||||
inspection.getVeiStartAt(),
|
||||
null,
|
||||
"NEW_PRETRIP_SAME_VEHICLE"
|
||||
);
|
||||
log.info(
|
||||
"[DISPATCH][CLOSE] reason={} dispatchUuid={} inspectionType={}",
|
||||
"NEW_PRETRIP_SAME_VEHICLE",
|
||||
open.getVedUuid(),
|
||||
inspection.getVeiInspectionType()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rule 3: same driver, different vehicle, preTrip
|
||||
if ("preTrip".equalsIgnoreCase(inspection.getVeiInspectionType())
|
||||
&& inspection.getVeiDriverId().equals(open.getVedDriverId())
|
||||
&& !inspection.getVeiVehId().equals(open.getVedVehId())) {
|
||||
|
||||
closeDispatch(
|
||||
open.getVedUuid(),
|
||||
inspection.getVeiStartAt(),
|
||||
null,
|
||||
"NEW_PRETRIP_OTHER_VEHICLE"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
* JOB / 60분 RULE로 배차 종료
|
||||
* =============================== */
|
||||
public void closePausedDispatches(Duration threshold) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<VehicleDispatch> paused =
|
||||
dispatchRepository.findPausedBefore(now.minus(threshold));
|
||||
|
||||
for (VehicleDispatch d : paused) {
|
||||
closeDispatch(
|
||||
d.getVedUuid(),
|
||||
now,
|
||||
null,
|
||||
"ENGINE_OFF_60MIN"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
* MANUAL 배차 업데이트
|
||||
* =============================== */
|
||||
public VehicleDispatch patchManualDispatch(
|
||||
UUID dispatchUuid,
|
||||
VehicleDispatchRequestDto dto,
|
||||
String updatedBy
|
||||
) {
|
||||
VehicleDispatch d = dispatchRepository.findByVedUuid(dispatchUuid)
|
||||
.orElseThrow(() -> new IllegalArgumentException("dispatch not found"));
|
||||
|
||||
if ("C".equals(d.getVedStatus())) {
|
||||
throw new IllegalStateException("closed dispatch cannot be modified");
|
||||
}
|
||||
|
||||
if (dto.getVedDriverId() != null) d.setVedDriverId(dto.getVedDriverId());
|
||||
if (dto.getVedSubDriverId() != null) d.setVedSubDriverId(dto.getVedSubDriverId());
|
||||
if (dto.getVedStartAt() != null) d.setVedStartAt(dto.getVedStartAt());
|
||||
if (dto.getVedOdometerSource() != null) d.setVedOdometerSource(dto.getVedOdometerSource());
|
||||
if (dto.getVedEventCount() != null) d.setVedEventCount(dto.getVedEventCount());
|
||||
|
||||
// odometer
|
||||
if (dto.getVedOdometerStart() != null) {
|
||||
d.setVedOdometerStart(dto.getVedOdometerStart());
|
||||
}
|
||||
if (dto.getVedOdometerEnd() != null) {
|
||||
d.setVedOdometerEnd(dto.getVedOdometerEnd());
|
||||
|
||||
if (d.getVedOdometerStart() != null) {
|
||||
d.setVedOdometerIncrement(
|
||||
dto.getVedOdometerEnd().subtract(d.getVedOdometerStart())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
d.setVedUpdatedAt(LocalDateTime.now());
|
||||
d.setVedUpdatedBy(updatedBy);
|
||||
|
||||
return dispatchRepository.save(d);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
private VehicleDispatch getOpen(UUID uuid) {
|
||||
VehicleDispatch d = dispatchRepository.findByVedUuid(uuid)
|
||||
.orElseThrow(() -> new IllegalArgumentException("dispatch not found"));
|
||||
|
||||
if (!"O".equals(d.getVedStatus())) {
|
||||
throw new IllegalStateException("dispatch not open");
|
||||
}
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.VehicleExternalMapResponseDto;
|
||||
import com.goi.erp.entity.VehicleExternalMap;
|
||||
import com.goi.erp.repository.VehicleExternalMapRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleExternalMapService {
|
||||
|
||||
private final VehicleExternalMapRepository externalMapRepository;
|
||||
|
||||
/**
|
||||
* 외부 솔루션의 ID로 내부 employee 매핑 정보 조회
|
||||
* 목적: customer_daily_order.driver_id 에 들어갈 내부 emp_id 조회
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public VehicleExternalMapResponseDto findMapping(String solutionType, String externalId) {
|
||||
|
||||
VehicleExternalMap map = externalMapRepository
|
||||
.findByVexSolutionTypeAndVexExternalIdAndVexStatus(
|
||||
solutionType,
|
||||
externalId,
|
||||
"A" // 활성 매핑만 사용
|
||||
)
|
||||
.orElseThrow(() ->
|
||||
new RuntimeException("Vehicle external mapping not found for externalId: " + externalId)
|
||||
);
|
||||
|
||||
// Entity → ResponseDto 변환
|
||||
VehicleExternalMapResponseDto dto = new VehicleExternalMapResponseDto();
|
||||
dto.setVexUuid(map.getVexUuid());
|
||||
dto.setVexVehicleId(map.getVexVehicleId());
|
||||
dto.setVexSolutionType(map.getVexSolutionType());
|
||||
dto.setVexExternalId(map.getVexExternalId());
|
||||
dto.setVexStatus(map.getVexStatus());
|
||||
dto.setVexCreatedBy(map.getVexCreatedBy());
|
||||
dto.setVexUpdatedBy(map.getVexUpdatedBy());
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package com.goi.erp.service;
|
||||
|
||||
import com.goi.erp.dto.VehicleInspectionDefectDto;
|
||||
import com.goi.erp.dto.VehicleInspectionDto;
|
||||
import com.goi.erp.entity.VehicleDispatch;
|
||||
import com.goi.erp.entity.VehicleInspection;
|
||||
import com.goi.erp.entity.VehicleInspectionDefect;
|
||||
import com.goi.erp.repository.VehicleDispatchRepository;
|
||||
import com.goi.erp.repository.VehicleInspectionDefectRepository;
|
||||
import com.goi.erp.repository.VehicleInspectionRepository;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VehicleInspectionService {
|
||||
|
||||
private final VehicleInspectionRepository inspectionRepository;
|
||||
private final VehicleInspectionDefectRepository defectRepository;
|
||||
private final VehicleDispatchRepository dispatchRepository;
|
||||
private final HcmEmployeeClient hcmEmployeeClient;
|
||||
private final VehicleExternalMapService vehicleExternalMapService;
|
||||
private final VehicleDispatchService vehicleDispatchService;
|
||||
// private final VehicleService vehicleService;
|
||||
|
||||
/**
|
||||
* Create or Update Vehicle Inspection (idempotent)
|
||||
* -> defects upsert
|
||||
* -> evaluate dispatch close by inspection
|
||||
* -> create dispatch (preTrip) if needed
|
||||
*/
|
||||
@Transactional
|
||||
public VehicleInspection saveInspection(VehicleInspectionDto dto) {
|
||||
VehicleInspection inspection = inspectionRepository
|
||||
.findByVeiSourceAndVeiSourceId(dto.getSource(), dto.getSourceId())
|
||||
.orElseGet(VehicleInspection::new);
|
||||
|
||||
// ===== ID resolve =====
|
||||
Long driverId = resolveDriverId(dto);
|
||||
Long subDriverId = resolveSubDriverId(dto);
|
||||
Long vehicleId = resolveVehicleId(dto);
|
||||
|
||||
log.info(
|
||||
"[INSPECTION] start source={} sourceId={} type={} vehId={} driverId={}",
|
||||
dto.getSource(),
|
||||
dto.getSourceId(),
|
||||
dto.getInspectionType(),
|
||||
vehicleId,
|
||||
driverId
|
||||
);
|
||||
|
||||
// ===== Inspection =====
|
||||
inspection.setVeiUuid(UUID.randomUUID());
|
||||
inspection.setVeiVehId(vehicleId);
|
||||
inspection.setVeiDriverId(driverId);
|
||||
inspection.setVeiSubDriverId(subDriverId);
|
||||
inspection.setVeiInspectionDate(dto.getInspectionDate());
|
||||
inspection.setVeiInspectionType(dto.getInspectionType());
|
||||
inspection.setVeiStartAt(dto.getStartAt());
|
||||
inspection.setVeiEndAt(dto.getEndAt());
|
||||
inspection.setVeiResult(dto.getResult());
|
||||
inspection.setVeiOdometer(dto.getOdometer());
|
||||
inspection.setVeiHasDefect(dto.getHasDefect());
|
||||
inspection.setVeiSource(dto.getSource());
|
||||
inspection.setVeiSourceId(dto.getSourceId());
|
||||
|
||||
inspection = inspectionRepository.save(inspection);
|
||||
|
||||
// ===== Defects =====
|
||||
if (dto.getDefects() != null) {
|
||||
saveDefects(inspection.getVeiId(), dto.getDefects());
|
||||
}
|
||||
|
||||
// ===== Dispatch close evaluation (Rule 1~3) =====
|
||||
// - postTrip이면 여기서 closeDispatch 호출됨
|
||||
// - preTrip new driver/vehicle 조건도 여기서 기존 open close됨
|
||||
vehicleDispatchService.evaluateDispatchCloseByInspection(inspection);
|
||||
|
||||
// ===== Dispatch create (preTrip) =====
|
||||
// - 이미 open dispatch가 있으면 생성하지 않음
|
||||
// - odometerStart = inspection.odometer (가능하면)
|
||||
if (shouldCreateDispatch(inspection)) {
|
||||
// open 여부
|
||||
boolean hasOpenDispatch = dispatchRepository.findOpenDispatchByVehId(inspection.getVeiVehId()).isPresent();
|
||||
|
||||
if (!hasOpenDispatch) {
|
||||
|
||||
BigDecimal odometerStart =
|
||||
(inspection.getVeiOdometer() != null)
|
||||
? BigDecimal.valueOf(inspection.getVeiOdometer())
|
||||
: null;
|
||||
|
||||
VehicleDispatch dispatch =
|
||||
vehicleDispatchService.createFromPreTripInspection(
|
||||
inspection.getVeiVehId(),
|
||||
inspection.getVeiDriverId(),
|
||||
inspection.getVeiSubDriverId(),
|
||||
inspection.getVeiInspectionDate(),
|
||||
inspection.getVeiStartAt(),
|
||||
odometerStart,
|
||||
"AUTO"
|
||||
);
|
||||
|
||||
// inspection에 dispatch 연결
|
||||
inspection.setVeiDispatchId(dispatch.getVedId());
|
||||
inspection = inspectionRepository.save(inspection);
|
||||
} else {
|
||||
log.info(
|
||||
"[DISPATCH][CREATE] skipped. open dispatch already exists. vehId={}",
|
||||
inspection.getVeiVehId()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.debug(
|
||||
"[DISPATCH][CREATE] skip. reason=shouldCreateDispatch=false inspectionId={}",
|
||||
inspection.getVeiId()
|
||||
);
|
||||
}
|
||||
|
||||
return inspection;
|
||||
}
|
||||
|
||||
/**
|
||||
* defects 저장 (externalDefectId 기준 upsert)
|
||||
* - externalDefectId가 null이면 "수기"로 간주 → 그냥 매번 INSERT 하면 중복 가능성이 커짐
|
||||
* => 수기는 (veiId + defectType + comment + createdAt) 조합으로 찾아
|
||||
*/
|
||||
private void saveDefects(Long veiId, List<VehicleInspectionDefectDto> defects) {
|
||||
// 없으면
|
||||
if (defects == null || defects.isEmpty()) return;
|
||||
|
||||
// defect 별
|
||||
for (VehicleInspectionDefectDto d : defects) {
|
||||
|
||||
VehicleInspectionDefect defect;
|
||||
|
||||
// 외부 defect id가 있으면 그걸로 idempotent
|
||||
if (d.getVidExternalDefectId() != null && !d.getVidExternalDefectId().isBlank()) {
|
||||
defect = defectRepository
|
||||
.findByVidVeiIdAndVidExternalDefectId(veiId, d.getVidExternalDefectId())
|
||||
.orElseGet(VehicleInspectionDefect::new);
|
||||
} else {
|
||||
// 수기 defect: 최소한의 중복 방지 (원하면 repository 메서드로 더 정교하게)
|
||||
defect = new VehicleInspectionDefect();
|
||||
}
|
||||
|
||||
defect.setVidVeiId(veiId);
|
||||
defect.setVidExternalDefectId(d.getVidExternalDefectId());
|
||||
defect.setVidDefectType(d.getDefectType());
|
||||
defect.setVidComment(d.getComment());
|
||||
defect.setVidIsResolved(d.getIsResolved());
|
||||
defect.setVidCreatedAt(d.getCreatedAt());
|
||||
defect.setVidResolvedAt(d.getResolvedAt());
|
||||
defect.setVidResolvedBy(resolveEmpId(d));
|
||||
|
||||
defectRepository.save(defect);
|
||||
}
|
||||
}
|
||||
|
||||
private Long resolveDriverId(VehicleInspectionDto dto) {
|
||||
|
||||
if (dto.getDriverId() != null) return dto.getDriverId();
|
||||
|
||||
if (dto.getExternalDriverId() != null) {
|
||||
return hcmEmployeeClient.getEmpIdFromExternalId(
|
||||
"SAMSARA",
|
||||
dto.getExternalDriverId()
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.getDriverUuid() != null) {
|
||||
return hcmEmployeeClient.getEmpIdFromUuid(dto.getDriverUuid());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long resolveSubDriverId(VehicleInspectionDto dto) {
|
||||
|
||||
if (dto.getSubDriverId() != null) return dto.getSubDriverId();
|
||||
|
||||
if (dto.getExternalSubDriverId() != null) {
|
||||
return hcmEmployeeClient.getEmpIdFromExternalId(
|
||||
"SAMSARA",
|
||||
dto.getExternalSubDriverId()
|
||||
);
|
||||
}
|
||||
|
||||
if (dto.getSubDriverUuid() != null) {
|
||||
return hcmEmployeeClient.getEmpIdFromUuid(dto.getSubDriverUuid());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long resolveVehicleId(VehicleInspectionDto dto) {
|
||||
|
||||
if (dto.getVehId() != null) return dto.getVehId();
|
||||
|
||||
if (dto.getExternalVehicleId() != null) {
|
||||
return vehicleExternalMapService
|
||||
.findMapping("SAMSARA", dto.getExternalVehicleId())
|
||||
.getVexVehicleId();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long resolveEmpId(VehicleInspectionDefectDto dto) {
|
||||
|
||||
if (dto.getResolvedBy() != null) return dto.getResolvedBy();
|
||||
|
||||
if (dto.getResolvedByExternalId() != null) {
|
||||
return hcmEmployeeClient.getEmpIdFromExternalId(
|
||||
"SAMSARA",
|
||||
dto.getResolvedByExternalId()
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean shouldCreateDispatch(VehicleInspection inspection) {
|
||||
return "preTrip".equalsIgnoreCase(inspection.getVeiInspectionType())
|
||||
&& inspection.getVeiDispatchId() == null
|
||||
&& inspection.getVeiVehId() != null
|
||||
&& inspection.getVeiDriverId() != null;
|
||||
}
|
||||
}
|
||||
|
|
@ -35,4 +35,7 @@ server:
|
|||
# ================================
|
||||
hcm:
|
||||
api:
|
||||
base-url: http://localhost:8081/hcm-rest-api
|
||||
base-url: http://localhost:8081/hcm-rest-api
|
||||
internal:
|
||||
integration:
|
||||
token: ${INTEGRATION_INTERNAL_TOKEN}
|
||||
Loading…
Reference in New Issue