feat(backend): /api/analysis stats + gear-detections 엔드포인트 추가
중국어선 감시 화면의 실데이터 연동을 위해 기존 /api/analysis 에 집계/ 필터 기능을 보강한다. - VesselAnalysisResult 엔티티에 violation_categories TEXT[] 매핑 추가 - VesselAnalysisResponse 에 violationCategories / bd09OffsetM / ucafScore / ucftScore / clusterId 5개 필드 노출 - /api/analysis/vessels 에 mmsiPrefix / minRiskScore / minFishingPct 필터 파라미터 추가 - /api/analysis/stats: MMSI별 최신 row 기준 단일 쿼리 COUNT FILTER 집계 (total/dark/spoofing/transship/risk별/zone별/fishing/avgRisk) - /api/analysis/gear-detections: gear_code/judgment NOT NULL 인 row MMSI 중복 제거 목록. 어구/어망 판별 탭 '자동탐지 결과' 섹션 연동용 - deprecated 스텁 /api/vessel-analysis 는 프론트 호출 제거 후 다음 릴리즈에서 삭제 예정 (이번 PR 에서는 유지)
This commit is contained in:
부모
14eb4c7ea3
커밋
820ed75585
@ -0,0 +1,27 @@
|
|||||||
|
package gc.mda.kcg.domain.analysis;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vessel_analysis_results 집계 응답.
|
||||||
|
* MMSI별 최신 row 기준으로 단일 쿼리 COUNT FILTER 집계한다.
|
||||||
|
*/
|
||||||
|
public record AnalysisStatsResponse(
|
||||||
|
long total,
|
||||||
|
long darkCount,
|
||||||
|
long spoofingCount,
|
||||||
|
long transshipCount,
|
||||||
|
long criticalCount,
|
||||||
|
long highCount,
|
||||||
|
long mediumCount,
|
||||||
|
long lowCount,
|
||||||
|
long territorialCount,
|
||||||
|
long contiguousCount,
|
||||||
|
long eezCount,
|
||||||
|
long fishingCount,
|
||||||
|
BigDecimal avgRiskScore,
|
||||||
|
OffsetDateTime windowStart,
|
||||||
|
OffsetDateTime windowEnd
|
||||||
|
) {
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package gc.mda.kcg.domain.analysis;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prediction 자동 어구 탐지 결과 응답 DTO.
|
||||||
|
* gear_code / gear_judgment 가 NOT NULL 인 row의 핵심 필드만 노출.
|
||||||
|
*/
|
||||||
|
public record GearDetectionResponse(
|
||||||
|
Long id,
|
||||||
|
String mmsi,
|
||||||
|
OffsetDateTime analyzedAt,
|
||||||
|
String vesselType,
|
||||||
|
String gearCode,
|
||||||
|
String gearJudgment,
|
||||||
|
String permitStatus,
|
||||||
|
String riskLevel,
|
||||||
|
Integer riskScore,
|
||||||
|
String zoneCode,
|
||||||
|
List<String> violationCategories
|
||||||
|
) {
|
||||||
|
public static GearDetectionResponse from(VesselAnalysisResult e) {
|
||||||
|
return new GearDetectionResponse(
|
||||||
|
e.getId(),
|
||||||
|
e.getMmsi(),
|
||||||
|
e.getAnalyzedAt(),
|
||||||
|
e.getVesselType(),
|
||||||
|
e.getGearCode(),
|
||||||
|
e.getGearJudgment(),
|
||||||
|
e.getPermitStatus(),
|
||||||
|
e.getRiskLevel(),
|
||||||
|
e.getRiskScore(),
|
||||||
|
e.getZoneCode(),
|
||||||
|
e.getViolationCategories()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import org.springframework.data.domain.PageRequest;
|
|||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -33,17 +34,52 @@ public class VesselAnalysisController {
|
|||||||
@RequestParam(required = false) String zoneCode,
|
@RequestParam(required = false) String zoneCode,
|
||||||
@RequestParam(required = false) String riskLevel,
|
@RequestParam(required = false) String riskLevel,
|
||||||
@RequestParam(required = false) Boolean isDark,
|
@RequestParam(required = false) Boolean isDark,
|
||||||
|
@RequestParam(required = false) String mmsiPrefix,
|
||||||
|
@RequestParam(required = false) Integer minRiskScore,
|
||||||
|
@RequestParam(required = false) BigDecimal minFishingPct,
|
||||||
@RequestParam(defaultValue = "1") int hours,
|
@RequestParam(defaultValue = "1") int hours,
|
||||||
@RequestParam(defaultValue = "0") int page,
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestParam(defaultValue = "50") int size
|
@RequestParam(defaultValue = "50") int size
|
||||||
) {
|
) {
|
||||||
OffsetDateTime after = OffsetDateTime.now().minusHours(hours);
|
OffsetDateTime after = OffsetDateTime.now().minusHours(hours);
|
||||||
return service.getAnalysisResults(
|
return service.getAnalysisResults(
|
||||||
mmsi, zoneCode, riskLevel, isDark, after,
|
mmsi, zoneCode, riskLevel, isDark,
|
||||||
|
mmsiPrefix, minRiskScore, minFishingPct, after,
|
||||||
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "analyzedAt"))
|
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "analyzedAt"))
|
||||||
).map(VesselAnalysisResponse::from);
|
).map(VesselAnalysisResponse::from);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MMSI별 최신 row 기준 집계 (단일 쿼리 COUNT FILTER).
|
||||||
|
* - hours: 윈도우 (기본 1시간)
|
||||||
|
* - mmsiPrefix: '412' 같은 MMSI prefix 필터 (선택)
|
||||||
|
*/
|
||||||
|
@GetMapping("/stats")
|
||||||
|
@RequirePermission(resource = "detection:dark-vessel", operation = "READ")
|
||||||
|
public AnalysisStatsResponse getStats(
|
||||||
|
@RequestParam(defaultValue = "1") int hours,
|
||||||
|
@RequestParam(required = false) String mmsiPrefix
|
||||||
|
) {
|
||||||
|
return service.getStats(hours, mmsiPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prediction 자동 어구 탐지 결과 목록.
|
||||||
|
* gear_code/gear_judgment NOT NULL 인 row만 MMSI 중복 제거 후 반환.
|
||||||
|
*/
|
||||||
|
@GetMapping("/gear-detections")
|
||||||
|
@RequirePermission(resource = "detection:dark-vessel", operation = "READ")
|
||||||
|
public Page<GearDetectionResponse> listGearDetections(
|
||||||
|
@RequestParam(defaultValue = "1") int hours,
|
||||||
|
@RequestParam(required = false) String mmsiPrefix,
|
||||||
|
@RequestParam(defaultValue = "0") int page,
|
||||||
|
@RequestParam(defaultValue = "50") int size
|
||||||
|
) {
|
||||||
|
return service.getGearDetections(hours, mmsiPrefix,
|
||||||
|
PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "analyzedAt"))
|
||||||
|
).map(GearDetectionResponse::from);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 선박 최신 분석 결과 (features 포함).
|
* 특정 선박 최신 분석 결과 (features 포함).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.springframework.data.repository.query.Param;
|
|||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,4 +58,61 @@ public interface VesselAnalysisRepository
|
|||||||
""")
|
""")
|
||||||
Page<VesselAnalysisResult> findLatestTransshipSuspects(
|
Page<VesselAnalysisResult> findLatestTransshipSuspects(
|
||||||
@Param("after") OffsetDateTime after, Pageable pageable);
|
@Param("after") OffsetDateTime after, Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 어구 탐지 결과 목록 (gear_code/judgment NOT NULL, MMSI 중복 제거).
|
||||||
|
* mmsiPrefix 가 null 이면 전체, 아니면 LIKE ':prefix%'.
|
||||||
|
*/
|
||||||
|
@Query("""
|
||||||
|
SELECT v FROM VesselAnalysisResult v
|
||||||
|
WHERE v.gearCode IS NOT NULL
|
||||||
|
AND v.gearJudgment IS NOT NULL
|
||||||
|
AND v.analyzedAt > :after
|
||||||
|
AND (:mmsiPrefix IS NULL OR v.mmsi LIKE CONCAT(:mmsiPrefix, '%'))
|
||||||
|
AND v.analyzedAt = (
|
||||||
|
SELECT MAX(v2.analyzedAt) FROM VesselAnalysisResult v2
|
||||||
|
WHERE v2.mmsi = v.mmsi AND v2.analyzedAt > :after
|
||||||
|
)
|
||||||
|
ORDER BY v.analyzedAt DESC
|
||||||
|
""")
|
||||||
|
Page<VesselAnalysisResult> findLatestGearDetections(
|
||||||
|
@Param("after") OffsetDateTime after,
|
||||||
|
@Param("mmsiPrefix") String mmsiPrefix,
|
||||||
|
Pageable pageable);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MMSI별 최신 row 기준 집계 (단일 쿼리 COUNT FILTER).
|
||||||
|
* mmsiPrefix 가 null 이면 전체.
|
||||||
|
* 반환 Map 키: total, dark_count, spoofing_count, transship_count,
|
||||||
|
* critical_count, high_count, medium_count, low_count,
|
||||||
|
* territorial_count, contiguous_count, eez_count,
|
||||||
|
* fishing_count, avg_risk_score
|
||||||
|
*/
|
||||||
|
@Query(value = """
|
||||||
|
WITH latest AS (
|
||||||
|
SELECT DISTINCT ON (mmsi) *
|
||||||
|
FROM kcg.vessel_analysis_results
|
||||||
|
WHERE analyzed_at > :after
|
||||||
|
AND (:mmsiPrefix IS NULL OR mmsi LIKE :mmsiPrefix || '%')
|
||||||
|
ORDER BY mmsi, analyzed_at DESC
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total,
|
||||||
|
COUNT(*) FILTER (WHERE is_dark = TRUE) AS dark_count,
|
||||||
|
COUNT(*) FILTER (WHERE spoofing_score >= 0.3) AS spoofing_count,
|
||||||
|
COUNT(*) FILTER (WHERE transship_suspect = TRUE) AS transship_count,
|
||||||
|
COUNT(*) FILTER (WHERE risk_level = 'CRITICAL') AS critical_count,
|
||||||
|
COUNT(*) FILTER (WHERE risk_level = 'HIGH') AS high_count,
|
||||||
|
COUNT(*) FILTER (WHERE risk_level = 'MEDIUM') AS medium_count,
|
||||||
|
COUNT(*) FILTER (WHERE risk_level = 'LOW') AS low_count,
|
||||||
|
COUNT(*) FILTER (WHERE zone_code = 'TERRITORIAL_SEA') AS territorial_count,
|
||||||
|
COUNT(*) FILTER (WHERE zone_code = 'CONTIGUOUS_ZONE') AS contiguous_count,
|
||||||
|
COUNT(*) FILTER (WHERE zone_code = 'EEZ_OR_BEYOND') AS eez_count,
|
||||||
|
COUNT(*) FILTER (WHERE fishing_pct > 0.5) AS fishing_count,
|
||||||
|
COALESCE(AVG(risk_score), 0)::NUMERIC(5,2) AS avg_risk_score
|
||||||
|
FROM latest
|
||||||
|
""", nativeQuery = true)
|
||||||
|
Map<String, Object> aggregateStats(
|
||||||
|
@Param("after") OffsetDateTime after,
|
||||||
|
@Param("mmsiPrefix") String mmsiPrefix);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package gc.mda.kcg.domain.analysis;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,6 +17,7 @@ public record VesselAnalysisResponse(
|
|||||||
String vesselType,
|
String vesselType,
|
||||||
BigDecimal confidence,
|
BigDecimal confidence,
|
||||||
BigDecimal fishingPct,
|
BigDecimal fishingPct,
|
||||||
|
Integer clusterId,
|
||||||
String season,
|
String season,
|
||||||
// 위치
|
// 위치
|
||||||
Double lat,
|
Double lat,
|
||||||
@ -24,11 +26,14 @@ public record VesselAnalysisResponse(
|
|||||||
BigDecimal distToBaselineNm,
|
BigDecimal distToBaselineNm,
|
||||||
// 행동
|
// 행동
|
||||||
String activityState,
|
String activityState,
|
||||||
|
BigDecimal ucafScore,
|
||||||
|
BigDecimal ucftScore,
|
||||||
// 위협
|
// 위협
|
||||||
Boolean isDark,
|
Boolean isDark,
|
||||||
Integer gapDurationMin,
|
Integer gapDurationMin,
|
||||||
String darkPattern,
|
String darkPattern,
|
||||||
BigDecimal spoofingScore,
|
BigDecimal spoofingScore,
|
||||||
|
BigDecimal bd09OffsetM,
|
||||||
Integer speedJumpCount,
|
Integer speedJumpCount,
|
||||||
// 환적
|
// 환적
|
||||||
Boolean transshipSuspect,
|
Boolean transshipSuspect,
|
||||||
@ -45,6 +50,7 @@ public record VesselAnalysisResponse(
|
|||||||
String gearCode,
|
String gearCode,
|
||||||
String gearJudgment,
|
String gearJudgment,
|
||||||
String permitStatus,
|
String permitStatus,
|
||||||
|
List<String> violationCategories,
|
||||||
// features
|
// features
|
||||||
Map<String, Object> features
|
Map<String, Object> features
|
||||||
) {
|
) {
|
||||||
@ -56,16 +62,20 @@ public record VesselAnalysisResponse(
|
|||||||
e.getVesselType(),
|
e.getVesselType(),
|
||||||
e.getConfidence(),
|
e.getConfidence(),
|
||||||
e.getFishingPct(),
|
e.getFishingPct(),
|
||||||
|
e.getClusterId(),
|
||||||
e.getSeason(),
|
e.getSeason(),
|
||||||
e.getLat(),
|
e.getLat(),
|
||||||
e.getLon(),
|
e.getLon(),
|
||||||
e.getZoneCode(),
|
e.getZoneCode(),
|
||||||
e.getDistToBaselineNm(),
|
e.getDistToBaselineNm(),
|
||||||
e.getActivityState(),
|
e.getActivityState(),
|
||||||
|
e.getUcafScore(),
|
||||||
|
e.getUcftScore(),
|
||||||
e.getIsDark(),
|
e.getIsDark(),
|
||||||
e.getGapDurationMin(),
|
e.getGapDurationMin(),
|
||||||
e.getDarkPattern(),
|
e.getDarkPattern(),
|
||||||
e.getSpoofingScore(),
|
e.getSpoofingScore(),
|
||||||
|
e.getBd09OffsetM(),
|
||||||
e.getSpeedJumpCount(),
|
e.getSpeedJumpCount(),
|
||||||
e.getTransshipSuspect(),
|
e.getTransshipSuspect(),
|
||||||
e.getTransshipPairMmsi(),
|
e.getTransshipPairMmsi(),
|
||||||
@ -78,6 +88,7 @@ public record VesselAnalysisResponse(
|
|||||||
e.getGearCode(),
|
e.getGearCode(),
|
||||||
e.getGearJudgment(),
|
e.getGearJudgment(),
|
||||||
e.getPermitStatus(),
|
e.getPermitStatus(),
|
||||||
|
e.getViolationCategories(),
|
||||||
e.getFeatures()
|
e.getFeatures()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import org.hibernate.type.SqlTypes;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,6 +126,10 @@ public class VesselAnalysisResult {
|
|||||||
@Column(name = "permit_status", length = 20)
|
@Column(name = "permit_status", length = 20)
|
||||||
private String permitStatus;
|
private String permitStatus;
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||||
|
@Column(name = "violation_categories", columnDefinition = "text[]")
|
||||||
|
private List<String> violationCategories;
|
||||||
|
|
||||||
// features JSONB
|
// features JSONB
|
||||||
@JdbcTypeCode(SqlTypes.JSON)
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
@Column(name = "features", columnDefinition = "jsonb")
|
@Column(name = "features", columnDefinition = "jsonb")
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* vessel_analysis_results 직접 조회 서비스.
|
* vessel_analysis_results 직접 조회 서비스.
|
||||||
@ -26,9 +28,10 @@ public class VesselAnalysisService {
|
|||||||
*/
|
*/
|
||||||
public Page<VesselAnalysisResult> getAnalysisResults(
|
public Page<VesselAnalysisResult> getAnalysisResults(
|
||||||
String mmsi, String zoneCode, String riskLevel, Boolean isDark,
|
String mmsi, String zoneCode, String riskLevel, Boolean isDark,
|
||||||
|
String mmsiPrefix, Integer minRiskScore, BigDecimal minFishingPct,
|
||||||
OffsetDateTime after, Pageable pageable
|
OffsetDateTime after, Pageable pageable
|
||||||
) {
|
) {
|
||||||
Specification<VesselAnalysisResult> spec = Specification.where(null);
|
Specification<VesselAnalysisResult> spec = (root, query, cb) -> cb.conjunction();
|
||||||
|
|
||||||
if (after != null) {
|
if (after != null) {
|
||||||
spec = spec.and((root, query, cb) -> cb.greaterThan(root.get("analyzedAt"), after));
|
spec = spec.and((root, query, cb) -> cb.greaterThan(root.get("analyzedAt"), after));
|
||||||
@ -36,6 +39,10 @@ public class VesselAnalysisService {
|
|||||||
if (mmsi != null && !mmsi.isBlank()) {
|
if (mmsi != null && !mmsi.isBlank()) {
|
||||||
spec = spec.and((root, query, cb) -> cb.equal(root.get("mmsi"), mmsi));
|
spec = spec.and((root, query, cb) -> cb.equal(root.get("mmsi"), mmsi));
|
||||||
}
|
}
|
||||||
|
if (mmsiPrefix != null && !mmsiPrefix.isBlank()) {
|
||||||
|
final String prefix = mmsiPrefix;
|
||||||
|
spec = spec.and((root, query, cb) -> cb.like(root.get("mmsi"), prefix + "%"));
|
||||||
|
}
|
||||||
if (zoneCode != null && !zoneCode.isBlank()) {
|
if (zoneCode != null && !zoneCode.isBlank()) {
|
||||||
spec = spec.and((root, query, cb) -> cb.equal(root.get("zoneCode"), zoneCode));
|
spec = spec.and((root, query, cb) -> cb.equal(root.get("zoneCode"), zoneCode));
|
||||||
}
|
}
|
||||||
@ -45,10 +52,66 @@ public class VesselAnalysisService {
|
|||||||
if (isDark != null && isDark) {
|
if (isDark != null && isDark) {
|
||||||
spec = spec.and((root, query, cb) -> cb.isTrue(root.get("isDark")));
|
spec = spec.and((root, query, cb) -> cb.isTrue(root.get("isDark")));
|
||||||
}
|
}
|
||||||
|
if (minRiskScore != null) {
|
||||||
|
spec = spec.and((root, query, cb) -> cb.greaterThanOrEqualTo(root.get("riskScore"), minRiskScore));
|
||||||
|
}
|
||||||
|
if (minFishingPct != null) {
|
||||||
|
spec = spec.and((root, query, cb) -> cb.greaterThan(root.get("fishingPct"), minFishingPct));
|
||||||
|
}
|
||||||
|
|
||||||
return repository.findAll(spec, pageable);
|
return repository.findAll(spec, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MMSI별 최신 row 기준 집계 (단일 쿼리).
|
||||||
|
*/
|
||||||
|
public AnalysisStatsResponse getStats(int hours, String mmsiPrefix) {
|
||||||
|
OffsetDateTime windowEnd = OffsetDateTime.now();
|
||||||
|
OffsetDateTime windowStart = windowEnd.minusHours(hours);
|
||||||
|
String prefix = (mmsiPrefix != null && !mmsiPrefix.isBlank()) ? mmsiPrefix : null;
|
||||||
|
|
||||||
|
Map<String, Object> row = repository.aggregateStats(windowStart, prefix);
|
||||||
|
return new AnalysisStatsResponse(
|
||||||
|
longOf(row, "total"),
|
||||||
|
longOf(row, "dark_count"),
|
||||||
|
longOf(row, "spoofing_count"),
|
||||||
|
longOf(row, "transship_count"),
|
||||||
|
longOf(row, "critical_count"),
|
||||||
|
longOf(row, "high_count"),
|
||||||
|
longOf(row, "medium_count"),
|
||||||
|
longOf(row, "low_count"),
|
||||||
|
longOf(row, "territorial_count"),
|
||||||
|
longOf(row, "contiguous_count"),
|
||||||
|
longOf(row, "eez_count"),
|
||||||
|
longOf(row, "fishing_count"),
|
||||||
|
bigDecimalOf(row, "avg_risk_score"),
|
||||||
|
windowStart,
|
||||||
|
windowEnd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prediction 자동 어구 탐지 결과 목록.
|
||||||
|
*/
|
||||||
|
public Page<VesselAnalysisResult> getGearDetections(int hours, String mmsiPrefix, Pageable pageable) {
|
||||||
|
OffsetDateTime after = OffsetDateTime.now().minusHours(hours);
|
||||||
|
String prefix = (mmsiPrefix != null && !mmsiPrefix.isBlank()) ? mmsiPrefix : null;
|
||||||
|
return repository.findLatestGearDetections(after, prefix, pageable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long longOf(Map<String, Object> row, String key) {
|
||||||
|
Object v = row.get(key);
|
||||||
|
if (v == null) return 0L;
|
||||||
|
return ((Number) v).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal bigDecimalOf(Map<String, Object> row, String key) {
|
||||||
|
Object v = row.get(key);
|
||||||
|
if (v == null) return BigDecimal.ZERO;
|
||||||
|
if (v instanceof BigDecimal bd) return bd;
|
||||||
|
return new BigDecimal(v.toString());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정 선박 최신 분석 결과.
|
* 특정 선박 최신 분석 결과.
|
||||||
*/
|
*/
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user