feat: ChnPrmShip 전용 DB 이력 + API enrichment + ShipImage V2

- t_chnprmship_positions 월별 파티션 테이블 (PartitionManager 관리)
- ChnPrmShipPositionSyncStep: 5분 Job 편승 캐시→DB 이중 적재
- ChnPrmShip 캐시 TTL 2→7일, 워밍업 소스 전용 DB + t_ais_position 이중화
- tracks/vessels API: includeChnPrmShip=true 시 ChnPrmShipInfo enrichment
- ShipImageControllerV2: /api/v2/shipimg/{imo} 추가
- SwaggerConfig: V2 경로 분리 + shipimg V2 그룹 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
htlee 2026-02-19 22:37:03 +09:00
부모 508f35a214
커밋 0cc6e58f9b
12개의 변경된 파일427개의 추가작업 그리고 29개의 파일을 삭제

파일 보기

@ -0,0 +1,119 @@
package gc.mda.signal_batch.batch.job;
import gc.mda.signal_batch.batch.reader.ChnPrmShipCacheManager;
import gc.mda.signal_batch.domain.vessel.model.AisTargetEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 5분 집계 Job 편승: ChnPrmShip 캐시 스냅샷 t_chnprmship_positions INSERT
*
* - ChnPrmShipCacheManager의 전체 스냅샷을 조회
* - ON CONFLICT (mmsi, message_timestamp) DO NOTHING: 동일 시점 중복 스킵, 시점은 이력 추가
* - 서비스 재시작 ChnPrmShipCacheWarmer가 테이블에서 워밍업
*/
@Slf4j
@Configuration
@Profile("!query")
@ConditionalOnProperty(name = "vessel.batch.scheduler.enabled", havingValue = "true", matchIfMissing = true)
public class ChnPrmShipPositionSyncStepConfig {
private final JobRepository jobRepository;
private final DataSource queryDataSource;
private final PlatformTransactionManager transactionManager;
private final ChnPrmShipCacheManager chnPrmShipCacheManager;
public ChnPrmShipPositionSyncStepConfig(
JobRepository jobRepository,
@Qualifier("queryDataSource") DataSource queryDataSource,
@Qualifier("queryTransactionManager") PlatformTransactionManager transactionManager,
ChnPrmShipCacheManager chnPrmShipCacheManager) {
this.jobRepository = jobRepository;
this.queryDataSource = queryDataSource;
this.transactionManager = transactionManager;
this.chnPrmShipCacheManager = chnPrmShipCacheManager;
}
@Bean
public Step chnPrmShipPositionSyncStep() {
return new StepBuilder("chnPrmShipPositionSyncStep", jobRepository)
.tasklet((contribution, chunkContext) -> {
Collection<AisTargetEntity> entities = chnPrmShipCacheManager.getAllValues();
if (entities.isEmpty()) {
log.debug("ChnPrmShip 캐시에 데이터 없음 — t_chnprmship_positions 동기화 스킵");
return org.springframework.batch.repeat.RepeatStatus.FINISHED;
}
JdbcTemplate jdbcTemplate = new JdbcTemplate(queryDataSource);
String sql = """
INSERT INTO signal.t_chnprmship_positions (
mmsi, imo, name, callsign, vessel_type, extra_info,
lat, lon, geom,
heading, sog, cog, rot,
length, width, draught,
destination, eta, status,
message_timestamp, signal_kind_code, class_type
) VALUES (
?, ?, ?, ?, ?, ?,
?, ?, public.ST_SetSRID(public.ST_MakePoint(?, ?), 4326),
?, ?, ?, ?,
?, ?, ?,
?, ?, ?,
?, ?, ?
)
ON CONFLICT (mmsi, message_timestamp) DO NOTHING
""";
List<Object[]> batchArgs = new ArrayList<>();
for (AisTargetEntity e : entities) {
if (e.getMmsi() == null || e.getLat() == null || e.getLon() == null
|| e.getMessageTimestamp() == null) {
continue;
}
Timestamp msgTs = Timestamp.from(e.getMessageTimestamp().toInstant());
Timestamp etaTs = e.getEta() != null
? Timestamp.from(e.getEta().toInstant())
: null;
batchArgs.add(new Object[] {
e.getMmsi(), e.getImo(), e.getName(), e.getCallsign(),
e.getVesselType(), e.getExtraInfo(),
e.getLat(), e.getLon(),
e.getLon(), e.getLat(), // ST_MakePoint(lon, lat)
e.getHeading(), e.getSog(), e.getCog(), e.getRot(),
e.getLength(), e.getWidth(), e.getDraught(),
e.getDestination(), etaTs, e.getStatus(),
msgTs, e.getSignalKindCode(), e.getClassType()
});
}
if (!batchArgs.isEmpty()) {
int[] results = jdbcTemplate.batchUpdate(sql, batchArgs);
log.info("t_chnprmship_positions 동기화 완료: {} 건 INSERT (DO NOTHING on conflict)",
results.length);
}
return org.springframework.batch.repeat.RepeatStatus.FINISHED;
}, transactionManager)
.build();
}
}

파일 보기

@ -26,10 +26,11 @@ public class VesselTrackAggregationJobConfig {
private final JobRepository jobRepository;
private final VesselTrackStepConfig vesselTrackStepConfig;
private final AisPositionSyncStepConfig aisPositionSyncStepConfig;
private final ChnPrmShipPositionSyncStepConfig chnPrmShipPositionSyncStepConfig;
private final JobCompletionListener jobCompletionListener;
private final CacheBasedTrackJobListener cacheBasedTrackJobListener;
private final PerformanceOptimizationListener performanceOptimizationListener;
@Bean
public Job vesselTrackAggregationJob() {
return new JobBuilder("vesselTrackAggregationJob", jobRepository)
@ -37,11 +38,12 @@ public class VesselTrackAggregationJobConfig {
.validator(trackJobParametersValidator())
.listener(jobCompletionListener)
.listener(cacheBasedTrackJobListener)
.listener(performanceOptimizationListener) // 성능 최적화 리스너 추가
.listener(performanceOptimizationListener)
.start(vesselTrackStepConfig.vesselTrackStep())
.next(vesselTrackStepConfig.gridTrackSummaryStep())
.next(vesselTrackStepConfig.areaTrackSummaryStep())
.next(aisPositionSyncStepConfig.aisPositionSyncStep())
.next(chnPrmShipPositionSyncStepConfig.chnPrmShipPositionSyncStep())
.build();
}

파일 보기

@ -10,6 +10,7 @@ import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -70,6 +71,21 @@ public class ChnPrmShipCacheManager {
return updated;
}
/**
* MMSI로 단건 조회 (enrichment용)
*/
public AisTargetEntity getByMmsi(String mmsi) {
if (mmsi == null) return null;
return cache.getIfPresent(mmsi);
}
/**
* 전체 캐시 스냅샷 반환 (DB 동기화용)
*/
public Collection<AisTargetEntity> getAllValues() {
return cache.asMap().values();
}
/**
* 시간 범위 캐시 데이터 조회
*/

파일 보기

@ -20,7 +20,9 @@ import java.util.List;
/**
* 기동 ChnPrmShip 캐시 워밍업
*
* t_ais_position 테이블에서 대상 MMSI의 데이터를 조회하여 캐시를 채운다.
* 1차: t_chnprmship_positions (전용 이력 테이블, 최근 7일)
* 2차: t_ais_position (최신 위치, fallback)
* MMSI별로 message_timestamp가 최신인 것을 캐시에 보관한다.
* 이후 배치 수집에서 실시간 데이터가 캐시를 갱신한다.
*/
@Slf4j
@ -57,34 +59,77 @@ public class ChnPrmShipCacheWarmer implements ApplicationRunner {
List<String> mmsiList = new ArrayList<>(properties.getMmsiSet());
int totalLoaded = 0;
// 1차: t_chnprmship_positions (전용 이력 테이블)
int loadedFromDedicated = 0;
for (int i = 0; i < mmsiList.size(); i += DB_QUERY_CHUNK_SIZE) {
List<String> chunk = mmsiList.subList(i,
Math.min(i + DB_QUERY_CHUNK_SIZE, mmsiList.size()));
try {
List<AisTargetEntity> fromDb = queryLatestByMmsiSince(chunk, since);
fromDb.forEach(entity -> {
if (entity.getSignalKindCode() == null) {
SignalKindCode kindCode = SignalKindCode.resolve(
entity.getVesselType(), entity.getExtraInfo());
entity.setSignalKindCode(kindCode.getCode());
}
});
cacheManager.putAll(fromDb);
totalLoaded += fromDb.size();
List<AisTargetEntity> fromDedicated = queryLatestFromChnPrmShipPositions(chunk, since);
resolveSignalKindCode(fromDedicated);
cacheManager.putAll(fromDedicated);
loadedFromDedicated += fromDedicated.size();
} catch (Exception e) {
log.warn("ChnPrmShip 워밍업 DB 조회 실패 (chunk {}/{}): {}",
log.warn("ChnPrmShip 워밍업 전용 테이블 조회 실패 (chunk {}/{}): {}",
i / DB_QUERY_CHUNK_SIZE + 1,
(mmsiList.size() + DB_QUERY_CHUNK_SIZE - 1) / DB_QUERY_CHUNK_SIZE,
e.getMessage());
}
}
totalLoaded += loadedFromDedicated;
// 2차: t_ais_position ( 최신 데이터가 있으면 덮어씀)
int loadedFromAisPosition = 0;
for (int i = 0; i < mmsiList.size(); i += DB_QUERY_CHUNK_SIZE) {
List<String> chunk = mmsiList.subList(i,
Math.min(i + DB_QUERY_CHUNK_SIZE, mmsiList.size()));
try {
List<AisTargetEntity> fromAis = queryLatestByMmsiSince(chunk, since);
resolveSignalKindCode(fromAis);
cacheManager.putAll(fromAis);
loadedFromAisPosition += fromAis.size();
} catch (Exception e) {
log.warn("ChnPrmShip 워밍업 t_ais_position 조회 실패 (chunk {}/{}): {}",
i / DB_QUERY_CHUNK_SIZE + 1,
(mmsiList.size() + DB_QUERY_CHUNK_SIZE - 1) / DB_QUERY_CHUNK_SIZE,
e.getMessage());
}
}
totalLoaded = Math.max(totalLoaded, loadedFromAisPosition);
long elapsed = System.currentTimeMillis() - startTime;
log.info("ChnPrmShip 캐시 워밍업 완료 - 대상: {}, 로딩: {}건, 소요: {}ms",
properties.getMmsiSet().size(), totalLoaded, elapsed);
log.info("ChnPrmShip 캐시 워밍업 완료 - 대상: {}, 전용DB: {}건, AIS: {}건, 캐시 크기: {}, 소요: {}ms",
properties.getMmsiSet().size(), loadedFromDedicated, loadedFromAisPosition,
cacheManager.size(), elapsed);
}
private void resolveSignalKindCode(List<AisTargetEntity> entities) {
entities.forEach(entity -> {
if (entity.getSignalKindCode() == null) {
SignalKindCode kindCode = SignalKindCode.resolve(
entity.getVesselType(), entity.getExtraInfo());
entity.setSignalKindCode(kindCode.getCode());
}
});
}
private List<AisTargetEntity> queryLatestFromChnPrmShipPositions(List<String> mmsiList, OffsetDateTime since) {
String placeholders = String.join(",", mmsiList.stream().map(m -> "?").toList());
String sql = "SELECT DISTINCT ON (mmsi) mmsi, imo, name, callsign, vessel_type, extra_info, " +
"lat, lon, heading, sog, cog, rot, length, width, draught, " +
"destination, eta, status, message_timestamp, signal_kind_code, class_type " +
"FROM signal.t_chnprmship_positions " +
"WHERE mmsi IN (" + placeholders + ") " +
"AND message_timestamp >= ? " +
"ORDER BY mmsi, message_timestamp DESC";
Object[] params = new Object[mmsiList.size() + 1];
for (int j = 0; j < mmsiList.size(); j++) {
params[j] = mmsiList.get(j);
}
params[mmsiList.size()] = since;
return queryJdbcTemplate.query(sql, params, (rs, rowNum) -> mapRow(rs));
}
private List<AisTargetEntity> queryLatestByMmsiSince(List<String> mmsiList, OffsetDateTime since) {

파일 보기

@ -168,6 +168,11 @@ public class GisControllerV2 {
- 선박별 병합된 단일 객체
- geometry/timestamps/speeds 배열 포함
- 선박 메타정보 포함
**중국허가선박 정보 (includeChnPrmShip)**
- `includeChnPrmShip: true` 설정 , 대상 MMSI에 대해 `chnPrmShipInfo` 필드가 추가됩니다
- 비대상 선박은 기존 응답과 동일 (chnPrmShipInfo: null JSON 자동 제외)
- 전용 캐시(TTL 7일)에서 조회하여 최신 위치/선박정보를 포함합니다
"""
)
@ApiResponses(value = {

파일 보기

@ -1,8 +1,11 @@
package gc.mda.signal_batch.domain.gis.service;
import gc.mda.signal_batch.batch.reader.ChnPrmShipCacheManager;
import gc.mda.signal_batch.batch.reader.ChnPrmShipProperties;
import gc.mda.signal_batch.batch.reader.FiveMinTrackCache;
import gc.mda.signal_batch.batch.reader.HourlyTrackCache;
import gc.mda.signal_batch.domain.vessel.dto.CompactVesselTrack;
import gc.mda.signal_batch.domain.vessel.model.AisTargetEntity;
import gc.mda.signal_batch.domain.vessel.dto.TrackResponse;
import gc.mda.signal_batch.domain.vessel.dto.VesselTracksRequest;
import gc.mda.signal_batch.domain.vessel.model.VesselTrack;
@ -46,6 +49,8 @@ public class GisServiceV2 {
private final HourlyTrackCache hourlyTrackCache;
private final FiveMinTrackCache fiveMinTrackCache;
private final VesselTrackToCompactConverter vesselTrackToCompactConverter;
private final ChnPrmShipCacheManager chnPrmShipCacheManager;
private final ChnPrmShipProperties chnPrmShipProperties;
@Value("${rest.v2.query.timeout-seconds:30}")
private int restQueryTimeout;
@ -64,7 +69,9 @@ public class GisServiceV2 {
GisService gisService,
HourlyTrackCache hourlyTrackCache,
FiveMinTrackCache fiveMinTrackCache,
VesselTrackToCompactConverter vesselTrackToCompactConverter) {
VesselTrackToCompactConverter vesselTrackToCompactConverter,
ChnPrmShipCacheManager chnPrmShipCacheManager,
ChnPrmShipProperties chnPrmShipProperties) {
this.queryDataSource = queryDataSource;
this.activeQueryManager = activeQueryManager;
this.dailyTrackCacheManager = dailyTrackCacheManager;
@ -73,6 +80,8 @@ public class GisServiceV2 {
this.hourlyTrackCache = hourlyTrackCache;
this.fiveMinTrackCache = fiveMinTrackCache;
this.vesselTrackToCompactConverter = vesselTrackToCompactConverter;
this.chnPrmShipCacheManager = chnPrmShipCacheManager;
this.chnPrmShipProperties = chnPrmShipProperties;
}
/**
@ -262,7 +271,7 @@ public class GisServiceV2 {
}
/**
* 선박별 항적 조회 V2 (캐시 + Semaphore + 간소화)
* 선박별 항적 조회 V2 (캐시 + Semaphore + 간소화 + ChnPrmShip enrichment)
*/
public List<CompactVesselTrack> getVesselTracksV2(VesselTracksRequest request) {
String queryId = "rest-vessels-" + UUID.randomUUID().toString().substring(0, 8);
@ -284,8 +293,14 @@ public class GisServiceV2 {
result = applySimplificationPipeline(result);
log.debug("V2 API: Returned {} tracks for {} vessels (cache={})",
result.size(), request.getVessels().size(), dailyTrackCacheManager.isEnabled());
// ChnPrmShip enrichment
if (request.isIncludeChnPrmShip()) {
result = enrichWithChnPrmShipInfo(result);
}
log.debug("V2 API: Returned {} tracks for {} vessels (cache={}, chnPrmShip={})",
result.size(), request.getVessels().size(),
dailyTrackCacheManager.isEnabled(), request.isIncludeChnPrmShip());
return result;
@ -497,6 +512,57 @@ public class GisServiceV2 {
return merged;
}
// ChnPrmShip Enrichment
private List<CompactVesselTrack> enrichWithChnPrmShipInfo(List<CompactVesselTrack> tracks) {
if (tracks == null || tracks.isEmpty()) {
return tracks;
}
int enriched = 0;
List<CompactVesselTrack> result = new ArrayList<>(tracks.size());
for (CompactVesselTrack track : tracks) {
if (!chnPrmShipProperties.isTarget(track.getVesselId())) {
result.add(track);
continue;
}
AisTargetEntity entity = chnPrmShipCacheManager.getByMmsi(track.getVesselId());
if (entity == null) {
result.add(track);
continue;
}
CompactVesselTrack.ChnPrmShipInfo info = CompactVesselTrack.ChnPrmShipInfo.builder()
.imo(entity.getImo())
.name(entity.getName())
.callsign(entity.getCallsign())
.vesselType(entity.getVesselType())
.lat(entity.getLat())
.lon(entity.getLon())
.sog(entity.getSog())
.cog(entity.getCog())
.heading(entity.getHeading())
.length(entity.getLength())
.width(entity.getWidth())
.draught(entity.getDraught())
.destination(entity.getDestination())
.status(entity.getStatus())
.messageTimestamp(entity.getMessageTimestamp())
.build();
result.add(track.toBuilder().chnPrmShipInfo(info).build());
enriched++;
}
if (enriched > 0) {
log.info("[ChnPrmShip] enrichment 완료: {} / {} tracks", enriched, tracks.size());
}
return result;
}
// MMSI 필터링
private Map<String, List<VesselTrack>> filterByMmsi(

파일 보기

@ -0,0 +1,60 @@
package gc.mda.signal_batch.domain.ship.controller;
import gc.mda.signal_batch.domain.ship.dto.ShipImageDto;
import gc.mda.signal_batch.domain.ship.service.ShipImageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 선박 이미지 API V2
* V1과 동일한 로직이며, V2 프록시 환경에서 별도 V1 프록시 설정 없이 사용하기 위해 분리
*/
@Slf4j
@RestController
@RequestMapping("/api/v2/shipimg")
@RequiredArgsConstructor
@Tag(name = "Ship Image V2", description = "선박 이미지 API (V2)")
public class ShipImageControllerV2 {
private final ShipImageService shipImageService;
@GetMapping("/{imo}")
@Operation(
summary = "선박 이미지 경로 조회",
description = "IMO 번호로 선박 이미지 경로 목록을 조회합니다. 프론트엔드에서 썸네일은 path + '_1.jpg', 원본은 path + '_2.jpg'를 사용합니다."
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공 (데이터 없으면 빈 배열 반환)",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ShipImageDto.class)))
)
})
public ResponseEntity<List<ShipImageDto>> getShipImages(
@Parameter(description = "IMO 번호", example = "9141833")
@PathVariable Integer imo) {
log.debug("V2 Requesting ship images for IMO: {}", imo);
List<ShipImageDto> images = shipImageService.getImagesByImo(imo);
log.debug("V2 Found {} images for IMO: {}", images.size(), imo);
return ResponseEntity.ok(images);
}
}

파일 보기

@ -9,6 +9,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.OffsetDateTime;
import java.util.List;
/**
@ -84,4 +85,61 @@ public class CompactVesselTrack {
example = "000023"
)
private String shipKindCode;
@Schema(description = "중국허가선박 정보 (대상 선박이고 includeChnPrmShip=true인 경우에만 포함)")
private ChnPrmShipInfo chnPrmShipInfo;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "중국허가선박 상세 정보 (AisTargetEntity 기반)")
public static class ChnPrmShipInfo {
@Schema(description = "IMO 번호", example = "9123456")
private Long imo;
@Schema(description = "선박명", example = "LIAN HE 15")
private String name;
@Schema(description = "호출부호", example = "BVZQ")
private String callsign;
@Schema(description = "선종 (AIS vessel type)", example = "Fishing")
private String vesselType;
@Schema(description = "위도", example = "34.227527")
private Double lat;
@Schema(description = "경도", example = "127.0638")
private Double lon;
@Schema(description = "대지속력 (knots)", example = "8.5")
private Double sog;
@Schema(description = "대지침로 (degrees)", example = "245.3")
private Double cog;
@Schema(description = "선수방향 (degrees)", example = "244.0")
private Double heading;
@Schema(description = "선박 길이 (m)", example = "45")
private Integer length;
@Schema(description = "선박 폭 (m)", example = "12")
private Integer width;
@Schema(description = "흘수 (m)", example = "3.5")
private Double draught;
@Schema(description = "목적지", example = "BUSAN")
private String destination;
@Schema(description = "항해 상태", example = "Under way using engine")
private String status;
@Schema(description = "위치 메시지 시각 (UTC)", example = "2026-02-19T12:00:00Z")
private OffsetDateTime messageTimestamp;
}
}

파일 보기

@ -44,4 +44,11 @@ public class VesselTracksRequest {
@Schema(description = "조회할 선박 MMSI 목록", required = true)
private List<String> vessels;
@Schema(
description = "중국허가선박 정보 포함 여부. true이면 대상 MMSI에 대해 chnPrmShipInfo를 응답에 추가",
example = "false"
)
@Builder.Default
private boolean includeChnPrmShip = false;
}

파일 보기

@ -80,7 +80,8 @@ public class SwaggerConfig {
.pathsToMatch(
"/api/v1/tracks/**", "/api/v1/haegu/**", "/api/v1/areas/**",
"/api/v1/passages/**", "/api/v1/vessels/**",
"/api/v2/**"
"/api/v2/tracks/**", "/api/v2/haegu/**", "/api/v2/areas/**",
"/api/v2/vessels/**"
)
.build();
}
@ -90,7 +91,7 @@ public class SwaggerConfig {
return GroupedOpenApi.builder()
.group("2-ship-image-api")
.displayName("선박 이미지 API")
.pathsToMatch("/api/v1/shipimg/**")
.pathsToMatch("/api/v1/shipimg/**", "/api/v2/shipimg/**")
.build();
}

파일 보기

@ -55,7 +55,8 @@ public class PartitionManager {
"t_grid_tracks_summary_daily",
"t_area_tracks_summary_hourly",
"t_area_tracks_summary_daily",
"t_abnormal_tracks"
"t_abnormal_tracks",
"t_chnprmship_positions"
);
/**
@ -409,6 +410,21 @@ public class PartitionManager {
partitionName, schema, partitionName
));
}
// 중국허가선박 위치 이력 테이블
else if (baseTable.equals("t_chnprmship_positions")) {
indexSqls.add(String.format(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS %s_mmsi_time_idx ON %s.%s (mmsi, message_timestamp DESC)",
partitionName, schema, partitionName
));
indexSqls.add(String.format(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS %s_time_idx ON %s.%s (message_timestamp DESC)",
partitionName, schema, partitionName
));
indexSqls.add(String.format(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS %s_geom_idx ON %s.%s USING GIST (geom)",
partitionName, schema, partitionName
));
}
// 인덱스 생성 실행
for (String indexSql : indexSqls) {
@ -662,7 +678,8 @@ public class PartitionManager {
OR tablename LIKE 't_grid%'
OR tablename LIKE 't_area%'
OR tablename LIKE 't_tile%'
OR tablename LIKE 't_abnormal%')
OR tablename LIKE 't_abnormal%'
OR tablename LIKE 't_chnprmship%')
ORDER BY tablename
""";

파일 보기

@ -208,6 +208,8 @@ vessel:
retention-months: 3 # 3개월 보관
t_abnormal_tracks:
retention-months: 0 # 무한 보관
t_chnprmship_positions:
retention-months: 3 # 3개월 보관
# Bulk Insert 설정
bulk-insert:
@ -290,10 +292,10 @@ app:
chnprmship:
mmsi-resource-path: classpath:chnprmship-mmsi.txt
ttl-days: 2
ttl-days: 7
max-size: 2000
warmup-enabled: true
warmup-days: 2
warmup-days: 7
# Swagger/OpenAPI 설정
springdoc: