feat: Swagger 문서 보강, Kafka 조건부 설정, AIS 응답 DTO 개선
- Swagger @Operation/@Schema 상세 설명 추가 (검색, 필터, 폴리곤 API) - Kafka 조건부 활성화 (KafkaAutoConfiguration exclude + @ConditionalOnProperty) - kafka.enabled=false일 때 Kafka 빈 미생성 (@Nullable 처리) - AisTargetResponseDto에 classType, core20Mmsi 필드 및 @Schema 추가 - ApiResponse에 @Schema 어노테이션 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
cfc80bbb0d
커밋
ce9244ca0a
@ -2,10 +2,11 @@ package com.snp.batch;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(exclude = KafkaAutoConfiguration.class)
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@ConfigurationPropertiesScan
|
@ConfigurationPropertiesScan
|
||||||
public class SnpBatchApplication {
|
public class SnpBatchApplication {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.snp.batch.common.web;
|
package com.snp.batch.common.web;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@ -14,26 +15,19 @@ import lombok.NoArgsConstructor;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "공통 API 응답 래퍼")
|
||||||
public class ApiResponse<T> {
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
/**
|
@Schema(description = "성공 여부", example = "true")
|
||||||
* 성공 여부
|
|
||||||
*/
|
|
||||||
private boolean success;
|
private boolean success;
|
||||||
|
|
||||||
/**
|
@Schema(description = "응답 메시지", example = "Success")
|
||||||
* 메시지
|
|
||||||
*/
|
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
/**
|
@Schema(description = "응답 데이터")
|
||||||
* 응답 데이터
|
|
||||||
*/
|
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
/**
|
@Schema(description = "에러 코드 (실패 시에만 존재)", example = "NOT_FOUND", nullable = true)
|
||||||
* 에러 코드 (실패 시)
|
|
||||||
*/
|
|
||||||
private String errorCode;
|
private String errorCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,9 +15,9 @@ import java.util.List;
|
|||||||
* Swagger/OpenAPI 3.0 설정
|
* Swagger/OpenAPI 3.0 설정
|
||||||
*
|
*
|
||||||
* Swagger UI 접속 URL:
|
* Swagger UI 접속 URL:
|
||||||
* - Swagger UI: http://localhost:8081/swagger-ui/index.html
|
* - Swagger UI: http://localhost:8041/snp-api/swagger-ui/index.html
|
||||||
* - API 문서 (JSON): http://localhost:8081/v3/api-docs
|
* - API 문서 (JSON): http://localhost:8041/snp-api/v3/api-docs
|
||||||
* - API 문서 (YAML): http://localhost:8081/v3/api-docs.yaml
|
* - API 문서 (YAML): http://localhost:8041/snp-api/v3/api-docs.yaml
|
||||||
*
|
*
|
||||||
* 주요 기능:
|
* 주요 기능:
|
||||||
* - REST API 자동 문서화
|
* - REST API 자동 문서화
|
||||||
@ -62,17 +62,19 @@ public class SwaggerConfig {
|
|||||||
.description("""
|
.description("""
|
||||||
## SNP Batch 시스템 REST API 문서
|
## SNP Batch 시스템 REST API 문서
|
||||||
|
|
||||||
Spring Batch 기반 데이터 통합 시스템의 REST API 문서입니다.
|
해양 데이터 통합 배치 시스템의 REST API 문서입니다.
|
||||||
|
|
||||||
### 제공 API
|
### 제공 API
|
||||||
- **Batch API**: 배치 Job 실행 및 관리
|
- **Batch Management API**: 배치 Job 실행, 이력 조회, 스케줄 관리
|
||||||
- **Product API**: 샘플 제품 데이터 CRUD (샘플용)
|
- **AIS Target API**: AIS 선박 위치 정보 조회 (캐시 기반, 공간/조건 검색)
|
||||||
|
|
||||||
### 주요 기능
|
### 주요 기능
|
||||||
- 배치 Job 실행 및 중지
|
- 배치 Job 실행 및 중지
|
||||||
- Job 실행 이력 조회
|
- Job 실행 이력 조회
|
||||||
- 스케줄 관리 (Quartz)
|
- 스케줄 관리 (Quartz)
|
||||||
- 제품 데이터 CRUD (샘플)
|
- AIS 선박 실시간 위치 조회 (MMSI 단건/다건, 시간/공간 범위 검색)
|
||||||
|
- 항해 조건 필터 검색 (SOG, COG, Heading, 목적지, 항행상태)
|
||||||
|
- 폴리곤/WKT 범위 검색, 거리 포함 검색, 항적 조회
|
||||||
|
|
||||||
### 버전 정보
|
### 버전 정보
|
||||||
- API Version: v1.0.0
|
- API Version: v1.0.0
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.snp.batch.jobs.aistarget.cache.AisTargetCacheManager;
|
|||||||
import com.snp.batch.jobs.aistarget.classifier.AisClassTypeClassifier;
|
import com.snp.batch.jobs.aistarget.classifier.AisClassTypeClassifier;
|
||||||
import com.snp.batch.jobs.aistarget.kafka.AisTargetKafkaProducer;
|
import com.snp.batch.jobs.aistarget.kafka.AisTargetKafkaProducer;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -16,11 +17,12 @@ import java.util.List;
|
|||||||
* 동작:
|
* 동작:
|
||||||
* 1. ClassType 분류 (Core20 캐시 기반 A/B 분류)
|
* 1. ClassType 분류 (Core20 캐시 기반 A/B 분류)
|
||||||
* 2. 캐시에 최신 위치 정보 업데이트 (classType, core20Mmsi 포함)
|
* 2. 캐시에 최신 위치 정보 업데이트 (classType, core20Mmsi 포함)
|
||||||
* 3. Kafka 토픽으로 AIS Target 정보 전송 (서브청크 분할)
|
* 3. Kafka 토픽으로 AIS Target 정보 전송 (서브청크 분할, 활성화된 경우에만)
|
||||||
*
|
*
|
||||||
* 참고:
|
* 참고:
|
||||||
* - DB 저장은 별도 Job(aisTargetDbSyncJob)에서 15분 주기로 수행
|
* - DB 저장은 별도 Job(aisTargetDbSyncJob)에서 15분 주기로 수행
|
||||||
* - Kafka 전송 실패는 기본적으로 로그만 남기고 다음 처리 계속
|
* - Kafka 전송 실패는 기본적으로 로그만 남기고 다음 처리 계속
|
||||||
|
* - Kafka가 비활성화(enabled=false)이면 kafkaProducer가 null이므로 전송 단계를 스킵
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -28,12 +30,13 @@ public class AisTargetDataWriter extends BaseWriter<AisTargetEntity> {
|
|||||||
|
|
||||||
private final AisTargetCacheManager cacheManager;
|
private final AisTargetCacheManager cacheManager;
|
||||||
private final AisClassTypeClassifier classTypeClassifier;
|
private final AisClassTypeClassifier classTypeClassifier;
|
||||||
|
@Nullable
|
||||||
private final AisTargetKafkaProducer kafkaProducer;
|
private final AisTargetKafkaProducer kafkaProducer;
|
||||||
|
|
||||||
public AisTargetDataWriter(
|
public AisTargetDataWriter(
|
||||||
AisTargetCacheManager cacheManager,
|
AisTargetCacheManager cacheManager,
|
||||||
AisClassTypeClassifier classTypeClassifier,
|
AisClassTypeClassifier classTypeClassifier,
|
||||||
AisTargetKafkaProducer kafkaProducer) {
|
@Nullable AisTargetKafkaProducer kafkaProducer) {
|
||||||
super("AisTarget");
|
super("AisTarget");
|
||||||
this.cacheManager = cacheManager;
|
this.cacheManager = cacheManager;
|
||||||
this.classTypeClassifier = classTypeClassifier;
|
this.classTypeClassifier = classTypeClassifier;
|
||||||
@ -54,9 +57,9 @@ public class AisTargetDataWriter extends BaseWriter<AisTargetEntity> {
|
|||||||
log.debug("AIS Target 캐시 업데이트 완료: {} 건 (캐시 크기: {})",
|
log.debug("AIS Target 캐시 업데이트 완료: {} 건 (캐시 크기: {})",
|
||||||
items.size(), cacheManager.size());
|
items.size(), cacheManager.size());
|
||||||
|
|
||||||
// 3. Kafka 전송 (설정 enabled=true 인 경우)
|
// 3. Kafka 전송 (kafkaProducer 빈이 존재하는 경우에만)
|
||||||
if (!kafkaProducer.isEnabled()) {
|
if (kafkaProducer == null) {
|
||||||
log.debug("AIS Kafka 전송 비활성화 - topic 전송 스킵");
|
log.debug("AIS Kafka Producer 미등록 - topic 전송 스킵");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
package com.snp.batch.jobs.aistarget.kafka;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka 조건부 활성화 설정
|
||||||
|
*
|
||||||
|
* SnpBatchApplication에서 KafkaAutoConfiguration을 기본 제외한 뒤,
|
||||||
|
* app.batch.ais-target.kafka.enabled=true인 경우에만 재활성화한다.
|
||||||
|
*
|
||||||
|
* enabled=false(기본값)이면 KafkaTemplate 등 Kafka 관련 빈이 전혀 생성되지 않는다.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
name = "app.batch.ais-target.kafka.enabled",
|
||||||
|
havingValue = "true"
|
||||||
|
)
|
||||||
|
@Import(KafkaAutoConfiguration.class)
|
||||||
|
public class AisTargetKafkaConfig {
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import com.snp.batch.jobs.aistarget.batch.entity.AisTargetEntity;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.kafka.core.KafkaTemplate;
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -21,9 +22,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
* - key: MMSI
|
* - key: MMSI
|
||||||
* - value: AisTargetKafkaMessage(JSON)
|
* - value: AisTargetKafkaMessage(JSON)
|
||||||
* - 실패 시 기본적으로 로그만 남기고 계속 진행 (failOnSendError=false)
|
* - 실패 시 기본적으로 로그만 남기고 계속 진행 (failOnSendError=false)
|
||||||
|
*
|
||||||
|
* app.batch.ais-target.kafka.enabled=true인 경우에만 빈으로 등록된다.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
name = "app.batch.ais-target.kafka.enabled",
|
||||||
|
havingValue = "true"
|
||||||
|
)
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AisTargetKafkaProducer {
|
public class AisTargetKafkaProducer {
|
||||||
|
|
||||||
|
|||||||
@ -7,13 +7,17 @@ import com.snp.batch.jobs.aistarget.web.dto.AisTargetSearchRequest;
|
|||||||
import com.snp.batch.jobs.aistarget.web.service.AisTargetService;
|
import com.snp.batch.jobs.aistarget.web.service.AisTargetService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -25,6 +29,7 @@ import java.util.Map;
|
|||||||
* - 캐시 미스 시 DB 조회 후 캐시 업데이트
|
* - 캐시 미스 시 DB 조회 후 캐시 업데이트
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/ais-target")
|
@RequestMapping("/api/ais-target")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -37,7 +42,11 @@ public class AisTargetController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "MMSI로 최신 위치 조회",
|
summary = "MMSI로 최신 위치 조회",
|
||||||
description = "특정 MMSI의 최신 위치 정보를 조회합니다 (캐시 우선)"
|
description = "특정 MMSI의 최신 위치 정보를 조회합니다 (캐시 우선)",
|
||||||
|
responses = {
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"),
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 MMSI의 위치 정보 없음")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@GetMapping("/{mmsi}")
|
@GetMapping("/{mmsi}")
|
||||||
public ResponseEntity<ApiResponse<AisTargetResponseDto>> getLatestByMmsi(
|
public ResponseEntity<ApiResponse<AisTargetResponseDto>> getLatestByMmsi(
|
||||||
@ -98,7 +107,7 @@ public class AisTargetController {
|
|||||||
@GetMapping("/search")
|
@GetMapping("/search")
|
||||||
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> search(
|
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> search(
|
||||||
@Parameter(description = "조회 범위 (분)", required = true, example = "5")
|
@Parameter(description = "조회 범위 (분)", required = true, example = "5")
|
||||||
@RequestParam Integer minutes,
|
@RequestParam @Min(value = 1, message = "minutes는 최소 1분 이상이어야 합니다") Integer minutes,
|
||||||
@Parameter(description = "중심 경도", example = "129.0")
|
@Parameter(description = "중심 경도", example = "129.0")
|
||||||
@RequestParam(required = false) Double centerLon,
|
@RequestParam(required = false) Double centerLon,
|
||||||
@Parameter(description = "중심 위도", example = "35.0")
|
@Parameter(description = "중심 위도", example = "35.0")
|
||||||
@ -128,6 +137,10 @@ public class AisTargetController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "시간/공간 범위로 선박 검색 (POST)",
|
summary = "시간/공간 범위로 선박 검색 (POST)",
|
||||||
|
responses = {
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "검색 성공"),
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 파라미터 검증 실패 (minutes 누락 또는 1 미만)")
|
||||||
|
},
|
||||||
description = """
|
description = """
|
||||||
POST 방식으로 검색 조건을 전달합니다.
|
POST 방식으로 검색 조건을 전달합니다.
|
||||||
|
|
||||||
@ -167,6 +180,10 @@ public class AisTargetController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "항해 조건 필터 검색",
|
summary = "항해 조건 필터 검색",
|
||||||
|
responses = {
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "필터 검색 성공"),
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 파라미터 검증 실패")
|
||||||
|
},
|
||||||
description = """
|
description = """
|
||||||
속도(SOG), 침로(COG), 선수방위(Heading), 목적지, 항행상태로 선박을 필터링합니다.
|
속도(SOG), 침로(COG), 선수방위(Heading), 목적지, 항행상태로 선박을 필터링합니다.
|
||||||
|
|
||||||
@ -218,30 +235,30 @@ public class AisTargetController {
|
|||||||
"headingCondition": "LT",
|
"headingCondition": "LT",
|
||||||
"headingValue": 180.0,
|
"headingValue": 180.0,
|
||||||
"destination": "BUSAN",
|
"destination": "BUSAN",
|
||||||
"statusList": ["0", "1", "5"]
|
"statusList": ["Under way using engine", "At anchor", "Moored"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
## 항행상태 코드 (statusList)
|
## 항행상태 값 (statusList)
|
||||||
|
|
||||||
| 코드 | 상태 |
|
statusList에는 **텍스트 문자열**을 전달해야 합니다 (대소문자 무시).
|
||||||
|
|
||||||
|
| 값 | 설명 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 0 | Under way using engine (기관 사용 항해 중) |
|
| Under way using engine | 기관 사용 항해 중 |
|
||||||
| 1 | At anchor (정박 중) |
|
| At anchor | 정박 중 |
|
||||||
| 2 | Not under command (조종불능) |
|
| Not under command | 조종불능 |
|
||||||
| 3 | Restricted manoeuverability (조종제한) |
|
| Restricted manoeuverability | 조종제한 |
|
||||||
| 4 | Constrained by her draught (흘수제약) |
|
| Constrained by her draught | 흘수제약 |
|
||||||
| 5 | Moored (계류 중) |
|
| Moored | 계류 중 |
|
||||||
| 6 | Aground (좌초) |
|
| Aground | 좌초 |
|
||||||
| 7 | Engaged in Fishing (어로 중) |
|
| Engaged in Fishing | 어로 중 |
|
||||||
| 8 | Under way sailing (돛 항해 중) |
|
| Under way sailing | 돛 항해 중 |
|
||||||
| 9-10 | Reserved for future use |
|
| Power Driven Towing Astern | 예인선 (후방) |
|
||||||
| 11 | Power-driven vessel towing astern |
|
| Power Driven Towing Alongside | 예인선 (측방) |
|
||||||
| 12 | Power-driven vessel pushing ahead |
|
| AIS Sart | 비상위치지시기 |
|
||||||
| 13 | Reserved for future use |
|
| N/A | 정보없음 |
|
||||||
| 14 | AIS-SART, MOB-AIS, EPIRB-AIS |
|
|
||||||
| 15 | Undefined (default) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
**참고:** 모든 필터는 선택사항이며, 미지정 시 해당 필드는 조건에서 제외됩니다 (전체 값 포함).
|
**참고:** 모든 필터는 선택사항이며, 미지정 시 해당 필드는 조건에서 제외됩니다 (전체 값 포함).
|
||||||
@ -269,6 +286,10 @@ public class AisTargetController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "폴리곤 범위 내 선박 검색",
|
summary = "폴리곤 범위 내 선박 검색",
|
||||||
|
responses = {
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "검색 성공"),
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 파라미터 검증 실패 (coordinates 또는 minutes 누락)")
|
||||||
|
},
|
||||||
description = """
|
description = """
|
||||||
폴리곤 범위 내 선박을 검색합니다.
|
폴리곤 범위 내 선박을 검색합니다.
|
||||||
|
|
||||||
@ -283,7 +304,7 @@ public class AisTargetController {
|
|||||||
)
|
)
|
||||||
@PostMapping("/search/polygon")
|
@PostMapping("/search/polygon")
|
||||||
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> searchByPolygon(
|
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> searchByPolygon(
|
||||||
@RequestBody PolygonSearchRequest request) {
|
@Valid @RequestBody PolygonSearchRequest request) {
|
||||||
log.info("폴리곤 검색 요청 - minutes: {}, points: {}",
|
log.info("폴리곤 검색 요청 - minutes: {}, points: {}",
|
||||||
request.getMinutes(), request.getCoordinates().length);
|
request.getMinutes(), request.getCoordinates().length);
|
||||||
|
|
||||||
@ -299,6 +320,10 @@ public class AisTargetController {
|
|||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "WKT 범위 내 선박 검색",
|
summary = "WKT 범위 내 선박 검색",
|
||||||
|
responses = {
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "검색 성공"),
|
||||||
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "요청 파라미터 검증 실패 (wkt 또는 minutes 누락)")
|
||||||
|
},
|
||||||
description = """
|
description = """
|
||||||
WKT(Well-Known Text) 형식으로 정의된 범위 내 선박을 검색합니다.
|
WKT(Well-Known Text) 형식으로 정의된 범위 내 선박을 검색합니다.
|
||||||
|
|
||||||
@ -313,7 +338,7 @@ public class AisTargetController {
|
|||||||
)
|
)
|
||||||
@PostMapping("/search/wkt")
|
@PostMapping("/search/wkt")
|
||||||
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> searchByWkt(
|
public ResponseEntity<ApiResponse<List<AisTargetResponseDto>>> searchByWkt(
|
||||||
@RequestBody WktSearchRequest request) {
|
@Valid @RequestBody WktSearchRequest request) {
|
||||||
log.info("WKT 검색 요청 - minutes: {}, wkt: {}", request.getMinutes(), request.getWkt());
|
log.info("WKT 검색 요청 - minutes: {}, wkt: {}", request.getMinutes(), request.getWkt());
|
||||||
|
|
||||||
List<AisTargetResponseDto> result = aisTargetService.searchByWkt(
|
List<AisTargetResponseDto> result = aisTargetService.searchByWkt(
|
||||||
@ -405,11 +430,17 @@ public class AisTargetController {
|
|||||||
* 폴리곤 검색 요청 DTO
|
* 폴리곤 검색 요청 DTO
|
||||||
*/
|
*/
|
||||||
@lombok.Data
|
@lombok.Data
|
||||||
|
@Schema(description = "폴리곤 범위 검색 요청")
|
||||||
public static class PolygonSearchRequest {
|
public static class PolygonSearchRequest {
|
||||||
@Parameter(description = "조회 범위 (분)", required = true, example = "5")
|
@NotNull(message = "minutes는 필수입니다")
|
||||||
private int minutes;
|
@Min(value = 1, message = "minutes는 최소 1분 이상이어야 합니다")
|
||||||
|
@Schema(description = "조회 범위 (분)", example = "5", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Integer minutes;
|
||||||
|
|
||||||
@Parameter(description = "폴리곤 좌표 [[lon, lat], ...]", required = true)
|
@NotNull(message = "coordinates는 필수입니다")
|
||||||
|
@Schema(description = "폴리곤 좌표 [[경도, 위도], ...] (닫힌 형태: 첫점=끝점)",
|
||||||
|
example = "[[129.0, 35.0], [130.0, 35.0], [130.0, 36.0], [129.0, 36.0], [129.0, 35.0]]",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private double[][] coordinates;
|
private double[][] coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,12 +448,17 @@ public class AisTargetController {
|
|||||||
* WKT 검색 요청 DTO
|
* WKT 검색 요청 DTO
|
||||||
*/
|
*/
|
||||||
@lombok.Data
|
@lombok.Data
|
||||||
|
@Schema(description = "WKT 범위 검색 요청")
|
||||||
public static class WktSearchRequest {
|
public static class WktSearchRequest {
|
||||||
@Parameter(description = "조회 범위 (분)", required = true, example = "5")
|
@NotNull(message = "minutes는 필수입니다")
|
||||||
private int minutes;
|
@Min(value = 1, message = "minutes는 최소 1분 이상이어야 합니다")
|
||||||
|
@Schema(description = "조회 범위 (분)", example = "5", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private Integer minutes;
|
||||||
|
|
||||||
@Parameter(description = "WKT 문자열", required = true,
|
@NotNull(message = "wkt는 필수입니다")
|
||||||
example = "POLYGON((129 35, 130 35, 130 36, 129 36, 129 35))")
|
@Schema(description = "WKT 문자열 (POLYGON, MULTIPOLYGON 지원)",
|
||||||
|
example = "POLYGON((129 35, 130 35, 130 36, 129 36, 129 35))",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String wkt;
|
private String wkt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,34 +22,66 @@ import java.time.OffsetDateTime;
|
|||||||
public class AisTargetResponseDto {
|
public class AisTargetResponseDto {
|
||||||
|
|
||||||
// 선박 식별 정보
|
// 선박 식별 정보
|
||||||
|
@Schema(description = "MMSI (Maritime Mobile Service Identity) 번호", example = "440123456")
|
||||||
private Long mmsi;
|
private Long mmsi;
|
||||||
|
|
||||||
|
@Schema(description = "IMO 번호 (0인 경우 미등록)", example = "9137960")
|
||||||
private Long imo;
|
private Long imo;
|
||||||
|
|
||||||
|
@Schema(description = "선박명", example = "ROYAUME DES OCEANS")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "호출 부호", example = "4SFTEST")
|
||||||
private String callsign;
|
private String callsign;
|
||||||
|
|
||||||
|
@Schema(description = "선박 유형 (외부 API 원본 텍스트)", example = "Vessel")
|
||||||
private String vesselType;
|
private String vesselType;
|
||||||
|
|
||||||
// 위치 정보
|
// 위치 정보
|
||||||
|
@Schema(description = "위도 (WGS84)", example = "35.0796")
|
||||||
private Double lat;
|
private Double lat;
|
||||||
|
|
||||||
|
@Schema(description = "경도 (WGS84)", example = "129.0756")
|
||||||
private Double lon;
|
private Double lon;
|
||||||
|
|
||||||
// 항해 정보
|
// 항해 정보
|
||||||
|
@Schema(description = "선수방위 (degrees, 0-360)", example = "36.0")
|
||||||
private Double heading;
|
private Double heading;
|
||||||
private Double sog; // Speed over Ground
|
|
||||||
private Double cog; // Course over Ground
|
@Schema(description = "대지속력 (knots)", example = "12.5")
|
||||||
private Integer rot; // Rate of Turn
|
private Double sog;
|
||||||
|
|
||||||
|
@Schema(description = "대지침로 (degrees, 0-360)", example = "36.2")
|
||||||
|
private Double cog;
|
||||||
|
|
||||||
|
@Schema(description = "회전율 (Rate of Turn)", example = "0")
|
||||||
|
private Integer rot;
|
||||||
|
|
||||||
// 선박 제원
|
// 선박 제원
|
||||||
|
@Schema(description = "선박 길이 (미터)", example = "19")
|
||||||
private Integer length;
|
private Integer length;
|
||||||
|
|
||||||
|
@Schema(description = "선박 폭 (미터)", example = "15")
|
||||||
private Integer width;
|
private Integer width;
|
||||||
|
|
||||||
|
@Schema(description = "흘수 (미터)", example = "5.5")
|
||||||
private Double draught;
|
private Double draught;
|
||||||
|
|
||||||
// 목적지 정보
|
// 목적지 정보
|
||||||
|
@Schema(description = "목적지", example = "BUSAN")
|
||||||
private String destination;
|
private String destination;
|
||||||
|
|
||||||
|
@Schema(description = "예정 도착 시간 (UTC)")
|
||||||
private OffsetDateTime eta;
|
private OffsetDateTime eta;
|
||||||
|
|
||||||
|
@Schema(description = "항행상태 (텍스트)", example = "Under way using engine")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
// 타임스탬프
|
// 타임스탬프
|
||||||
|
@Schema(description = "AIS 메시지 발생 시각 (UTC)")
|
||||||
private OffsetDateTime messageTimestamp;
|
private OffsetDateTime messageTimestamp;
|
||||||
|
|
||||||
|
@Schema(description = "데이터 수신 시각 (UTC)")
|
||||||
private OffsetDateTime receivedDate;
|
private OffsetDateTime receivedDate;
|
||||||
|
|
||||||
// 데이터 소스 (캐시/DB)
|
// 데이터 소스 (캐시/DB)
|
||||||
|
|||||||
@ -117,7 +117,7 @@ app:
|
|||||||
schedule:
|
schedule:
|
||||||
cron: "15 * * * * ?" # 매 분 15초 실행
|
cron: "15 * * * * ?" # 매 분 15초 실행
|
||||||
kafka:
|
kafka:
|
||||||
enabled: true
|
enabled: false
|
||||||
topic: tp_Global_AIS_Signal
|
topic: tp_Global_AIS_Signal
|
||||||
send-chunk-size: 5000
|
send-chunk-size: 5000
|
||||||
fail-on-send-error: false
|
fail-on-send-error: false
|
||||||
|
|||||||
@ -169,7 +169,7 @@ app:
|
|||||||
schedule:
|
schedule:
|
||||||
cron: "15 * * * * ?" # 매 분 15초 실행
|
cron: "15 * * * * ?" # 매 분 15초 실행
|
||||||
kafka:
|
kafka:
|
||||||
enabled: true
|
enabled: false # true로 변경 시 Kafka 브로커 연결 필요
|
||||||
topic: tp_Global_AIS_Signal
|
topic: tp_Global_AIS_Signal
|
||||||
send-chunk-size: 5000
|
send-chunk-size: 5000
|
||||||
fail-on-send-error: false
|
fail-on-send-error: false
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user