diff --git a/src/main/java/gc/mda/signal_batch/domain/gis/controller/MapTileController.java b/src/main/java/gc/mda/signal_batch/domain/gis/controller/MapTileController.java index 0960000..93ee113 100644 --- a/src/main/java/gc/mda/signal_batch/domain/gis/controller/MapTileController.java +++ b/src/main/java/gc/mda/signal_batch/domain/gis/controller/MapTileController.java @@ -1,5 +1,9 @@ package gc.mda.signal_batch.domain.gis.controller; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -17,16 +21,20 @@ import java.nio.file.Paths; @RestController @RequestMapping("/api/tiles") +@Tag(name = "지도 타일 API", description = "WebP 형식의 배경 지도 타일 서비스 (World/ENC)") public class MapTileController { private static final String TILE_BASE_PATH = "/devdata/MAPS/WORLD_webp"; private static final String TILE_ENC_PATH = "/devdata/MAPS/ENC_RAS_webp"; @GetMapping("/world/{z}/{x}/{y}.webp") + @Operation(summary = "세계지도 타일 조회", description = "ZXY 좌표 기반 세계지도 WebP 타일을 반환합니다") + @ApiResponse(responseCode = "200", description = "WebP 이미지 타일") + @ApiResponse(responseCode = "404", description = "타일 없음") public ResponseEntity getWorldTile( - @PathVariable int z, - @PathVariable int x, - @PathVariable int y) { + @Parameter(description = "줌 레벨 (0~18)") @PathVariable int z, + @Parameter(description = "X 좌표 (열)") @PathVariable int x, + @Parameter(description = "Y 좌표 (행)") @PathVariable int y) { try { // 안전한 경로 생성 @@ -59,10 +67,13 @@ public class MapTileController { } @GetMapping("/enc/{z}/{x}/{y}.webp") + @Operation(summary = "ENC 해도 타일 조회", description = "ZXY 좌표 기반 전자해도(ENC) WebP 타일을 반환합니다") + @ApiResponse(responseCode = "200", description = "WebP 이미지 타일") + @ApiResponse(responseCode = "404", description = "타일 없음") public ResponseEntity getEncTile( - @PathVariable int z, - @PathVariable int x, - @PathVariable int y) { + @Parameter(description = "줌 레벨 (0~18)") @PathVariable int z, + @Parameter(description = "X 좌표 (열)") @PathVariable int x, + @Parameter(description = "Y 좌표 (행)") @PathVariable int y) { try { // 안전한 경로 생성 @@ -95,6 +106,7 @@ public class MapTileController { } @GetMapping("/health") + @Operation(summary = "타일 서비스 상태 확인", description = "타일 디렉토리 존재 여부로 서비스 상태를 확인합니다") public ResponseEntity checkTileService() { File baseDir = new File(TILE_BASE_PATH); if (baseDir.exists() && baseDir.isDirectory()) { diff --git a/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java b/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java index d1ef9f9..f378374 100644 --- a/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java +++ b/src/main/java/gc/mda/signal_batch/domain/vessel/service/VesselPositionService.java @@ -12,6 +12,8 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @@ -155,10 +157,10 @@ public class VesselPositionService { return RecentVesselPositionDto.builder() .mmsi(mmsi) .imo(imo > 0 ? imo : null) - .lon(rs.getDouble("lon")) - .lat(rs.getDouble("lat")) - .sog(rs.getBigDecimal("sog")) - .cog(rs.getBigDecimal("cog")) + .lon(Math.round(rs.getDouble("lon") * 1_000_000) / 1_000_000.0) + .lat(Math.round(rs.getDouble("lat") * 1_000_000) / 1_000_000.0) + .sog(scaleDecimal(rs.getBigDecimal("sog"), 1)) + .cog(scaleDecimal(rs.getBigDecimal("cog"), 1)) .shipNm(rs.getString("ship_nm")) .shipTy(shipTy) .shipKindCode(shipKindCode) @@ -168,4 +170,8 @@ public class VesselPositionService { .build(); } } + + private static BigDecimal scaleDecimal(BigDecimal value, int scale) { + return value != null ? value.setScale(scale, RoundingMode.HALF_UP) : null; + } } diff --git a/src/main/java/gc/mda/signal_batch/global/config/JacksonConfig.java b/src/main/java/gc/mda/signal_batch/global/config/JacksonConfig.java index b733bab..219110d 100644 --- a/src/main/java/gc/mda/signal_batch/global/config/JacksonConfig.java +++ b/src/main/java/gc/mda/signal_batch/global/config/JacksonConfig.java @@ -1,5 +1,6 @@ package gc.mda.signal_batch.global.config; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -36,6 +37,7 @@ public class JacksonConfig { return Jackson2ObjectMapperBuilder.json() .modules(javaTimeModule) .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .serializationInclusion(JsonInclude.Include.NON_NULL) .build(); } } \ No newline at end of file diff --git a/src/main/java/gc/mda/signal_batch/monitoring/controller/BatchAdminController.java b/src/main/java/gc/mda/signal_batch/monitoring/controller/BatchAdminController.java index 9214dbe..14449e8 100644 --- a/src/main/java/gc/mda/signal_batch/monitoring/controller/BatchAdminController.java +++ b/src/main/java/gc/mda/signal_batch/monitoring/controller/BatchAdminController.java @@ -3,6 +3,10 @@ package gc.mda.signal_batch.monitoring.controller; import gc.mda.signal_batch.monitoring.service.BatchMetadataCleanupService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -641,7 +645,35 @@ public class BatchAdminController { * 프론트엔드에서 Stacked Bar (처리 건수) + Duration Line (소요시간) 차트 표시. */ @GetMapping("/daily-stats") - @Operation(summary = "일별 처리 통계", description = "최근 7일간 일별 Job별 배치 처리 통계를 조회합니다 (대시보드 차트용)") + @Operation( + summary = "일별 처리 통계", + description = "최근 7일간 일별 Job별 배치 처리 통계를 조회합니다 (대시보드 차트용)" + ) + @ApiResponse( + responseCode = "200", + description = "일별 통계 + 24시간 상태 요약", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Object.class), + examples = @ExampleObject(value = """ + { + "dailyStats": [ + { + "date": "2026-02-28", + "totalWrite": 285000, + "jobs": { + "incrementalVesselJob": { "writeCount": 120000, "execCount": 288, "avgDuration": 45 }, + "trackBuildJob": { "writeCount": 95000, "execCount": 288, "avgDuration": 30 }, + "hourlyAggregationJob": { "writeCount": 50000, "execCount": 24, "avgDuration": 15 }, + "dailyAggregationJob": { "writeCount": 20000, "execCount": 1, "avgDuration": 40 } + } + } + ], + "statusSummary": { "completed": 601, "failed": 0, "stopped": 0 } + } + """) + ) + ) public ResponseEntity> getDailyStatistics() { try { long start = System.currentTimeMillis(); diff --git a/src/main/java/gc/mda/signal_batch/monitoring/controller/LockMonitorController.java b/src/main/java/gc/mda/signal_batch/monitoring/controller/LockMonitorController.java index 20ac97e..036dca9 100644 --- a/src/main/java/gc/mda/signal_batch/monitoring/controller/LockMonitorController.java +++ b/src/main/java/gc/mda/signal_batch/monitoring/controller/LockMonitorController.java @@ -1,6 +1,8 @@ package gc.mda.signal_batch.monitoring.controller; import gc.mda.signal_batch.global.util.ConcurrentUpdateManager; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -13,11 +15,13 @@ import java.util.Map; @RestController @RequestMapping("/admin/locks") @RequiredArgsConstructor +@Tag(name = "Lock 모니터링 API", description = "배치 동시성 Lock 상태 및 Deadlock 정보 조회") public class LockMonitorController { private final ConcurrentUpdateManager concurrentUpdateManager; @GetMapping("/statistics") + @Operation(summary = "Lock 통계 조회", description = "현재 활성 Lock 및 Lock 사용 통계를 조회합니다") public ResponseEntity> getLockStatistics() { Map response = new HashMap<>(); response.put("lockStats", concurrentUpdateManager.getLockStatistics()); @@ -28,6 +32,7 @@ public class LockMonitorController { } @GetMapping("/deadlocks") + @Operation(summary = "Deadlock 정보 조회", description = "감지된 Deadlock 이력 정보를 조회합니다") public ResponseEntity> getDeadlockInfo() { Map response = new HashMap<>(); response.put("deadlocks", concurrentUpdateManager.getDeadlockInfo()); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bd0eb1b..174ecf8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,6 +2,10 @@ server: shutdown: graceful servlet: context-path: /signal-batch + compression: + enabled: true + mime-types: application/json,application/xml,text/html,text/xml,text/plain + min-response-size: 1024 spring: application: