- 파티션 관리 job 추가 (+3일 미리 생성, 14일 이전 파티션 자동drop 설정)
- (임시) GPU 운영 포트 9000번 변경
- ais_target 테이블 일일 파티션구조로 변경 (1일 데이터 약 20GB)
This commit is contained in:
HeungTak Lee 2025-12-04 13:05:00 +09:00
부모 5857a4a822
커밋 55d4dd5886
7개의 변경된 파일459개의 추가작업 그리고 187개의 파일을 삭제

파일 보기

@ -30,23 +30,29 @@ public class SwaggerConfig {
@Value("${server.port:8081}") @Value("${server.port:8081}")
private int serverPort; private int serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
return new OpenAPI() return new OpenAPI()
.info(apiInfo()) .info(apiInfo())
.servers(List.of( .servers(List.of(
new Server() new Server()
.url("http://localhost:" + serverPort) .url("http://localhost:" + serverPort + contextPath)
.description("로컬 개발 서버"), .description("로컬 개발 서버"),
new Server() new Server()
.url("http://10.26.252.39:" + serverPort) .url("http://10.26.252.39:" + serverPort + contextPath)
.description("로컬 개발 서버"), .description("로컬 개발 서버"),
new Server() new Server()
.url("http://211.208.115.83:" + serverPort) .url("http://211.208.115.83:" + serverPort + contextPath)
.description("중계 서버"), .description("중계 서버"),
new Server() new Server()
.url("http://10.187.58.58:" + serverPort) .url("http://10.187.58.58:" + serverPort + contextPath)
.description("운영 서버") .description("운영 서버"),
new Server()
.url("https://mda.kcg.go.kr" + contextPath)
.description("운영 서버 프록시")
)); ));
} }

파일 보기

@ -1,50 +1,133 @@
package com.snp.batch.global.partition; package com.snp.batch.global.partition;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* 파티션 관리 대상 테이블 설정 * 파티션 관리 설정 (application.yml 기반)
* *
* Daily 파티션: 매일 실행 * 설정 예시:
* Monthly 파티션: 매월 말일에만 실행 * app.batch.partition:
* daily-tables:
* - schema: snp_data
* table-name: ais_target
* partition-column: message_timestamp
* periods-ahead: 3
* monthly-tables:
* - schema: snp_data
* table-name: some_table
* partition-column: created_at
* periods-ahead: 2
* retention:
* daily-default-days: 14
* monthly-default-months: 1
* custom:
* - table-name: ais_target
* retention-days: 30
*/ */
@Getter @Getter
@Setter
@Component @Component
@ConfigurationProperties(prefix = "app.batch.partition")
public class PartitionConfig { public class PartitionConfig {
/** /**
* Daily 파티션 대상 테이블 (파티션 네이밍: {table}_YYYY_MM_DD) * 일별 파티션 테이블 목록 (파티션 네이밍: {table}_YYMMDD)
*/ */
private final List<PartitionTableInfo> dailyPartitionTables = List.of( private List<PartitionTableConfig> dailyTables = new ArrayList<>();
// 추후 daily 파티션 테이블 추가
);
/** /**
* Monthly 파티션 대상 테이블 (파티션 네이밍: {table}_YYYY_MM) * 월별 파티션 테이블 목록 (파티션 네이밍: {table}_YYYY_MM)
*/ */
private final List<PartitionTableInfo> monthlyPartitionTables = List.of( private List<PartitionTableConfig> monthlyTables = new ArrayList<>();
new PartitionTableInfo(
"snp_data",
"ais_target",
"message_timestamp",
2 // 미리 생성할 개월
)
);
/** /**
* 파티션 테이블 정보 * 보관기간 설정
*/ */
public record PartitionTableInfo( private RetentionConfig retention = new RetentionConfig();
String schema,
String tableName, /**
String partitionColumn, * 파티션 테이블 설정
int periodsAhead // 미리 생성할 기간 (daily: , monthly: ) */
) { @Getter
@Setter
public static class PartitionTableConfig {
private String schema = "snp_data";
private String tableName;
private String partitionColumn;
private int periodsAhead = 3; // 미리 생성할 기간 (daily: , monthly: )
public String getFullTableName() { public String getFullTableName() {
return schema + "." + tableName; return schema + "." + tableName;
} }
} }
/**
* 보관기간 설정
*/
@Getter
@Setter
public static class RetentionConfig {
/**
* 일별 파티션 기본 보관기간 ()
*/
private int dailyDefaultDays = 14;
/**
* 월별 파티션 기본 보관기간 (개월)
*/
private int monthlyDefaultMonths = 1;
/**
* 개별 테이블 보관기간 설정
*/
private List<CustomRetention> custom = new ArrayList<>();
}
/**
* 개별 테이블 보관기간 설정
*/
@Getter
@Setter
public static class CustomRetention {
private String tableName;
private Integer retentionDays; // 단위 보관기간 (일별 파티션용)
private Integer retentionMonths; // 단위 보관기간 (월별 파티션용)
}
/**
* 일별 파티션 테이블의 보관기간 조회 ( 단위)
*/
public int getDailyRetentionDays(String tableName) {
return getCustomRetention(tableName)
.map(c -> c.getRetentionDays() != null ? c.getRetentionDays() : retention.getDailyDefaultDays())
.orElse(retention.getDailyDefaultDays());
}
/**
* 월별 파티션 테이블의 보관기간 조회 ( 단위)
*/
public int getMonthlyRetentionMonths(String tableName) {
return getCustomRetention(tableName)
.map(c -> c.getRetentionMonths() != null ? c.getRetentionMonths() : retention.getMonthlyDefaultMonths())
.orElse(retention.getMonthlyDefaultMonths());
}
/**
* 개별 테이블 보관기간 설정 조회
*/
private Optional<CustomRetention> getCustomRetention(String tableName) {
if (retention.getCustom() == null) {
return Optional.empty();
}
return retention.getCustom().stream()
.filter(c -> tableName.equals(c.getTableName()))
.findFirst();
}
} }

파일 보기

@ -1,6 +1,6 @@
package com.snp.batch.global.partition; package com.snp.batch.global.partition;
import com.snp.batch.global.partition.PartitionConfig.PartitionTableInfo; import com.snp.batch.global.partition.PartitionConfig.PartitionTableConfig;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.StepContribution;
@ -20,8 +20,12 @@ import java.util.List;
* 파티션 관리 Tasklet * 파티션 관리 Tasklet
* *
* 스케줄: 매일 실행 * 스케줄: 매일 실행
* - Daily 파티션: 매일 생성 * - Daily 파티션: 매일 생성/삭제 (네이밍: {table}_YYMMDD)
* - Monthly 파티션: 매월 말일에만 생성 * - Monthly 파티션: 매월 말일에만 생성/삭제 (네이밍: {table}_YYYY_MM)
*
* 보관기간:
* - 기본값: 일별 14일, 월별 1개월
* - 개별 테이블별 보관기간 설정 가능 (application.yml)
*/ */
@Slf4j @Slf4j
@Component @Component
@ -31,6 +35,9 @@ public class PartitionManagerTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final PartitionConfig partitionConfig; private final PartitionConfig partitionConfig;
private static final DateTimeFormatter DAILY_PARTITION_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
private static final DateTimeFormatter MONTHLY_PARTITION_FORMAT = DateTimeFormatter.ofPattern("yyyy_MM");
private static final String PARTITION_EXISTS_SQL = """ private static final String PARTITION_EXISTS_SQL = """
SELECT EXISTS ( SELECT EXISTS (
SELECT 1 FROM pg_class c SELECT 1 FROM pg_class c
@ -41,6 +48,17 @@ public class PartitionManagerTasklet implements Tasklet {
) )
"""; """;
private static final String FIND_PARTITIONS_SQL = """
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_inherits i ON i.inhrelid = c.oid
WHERE n.nspname = ?
AND c.relname LIKE ?
AND c.relkind = 'r'
ORDER BY c.relname
""";
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
LocalDate today = LocalDate.now(); LocalDate today = LocalDate.now();
@ -52,14 +70,24 @@ public class PartitionManagerTasklet implements Tasklet {
log.info("월 말일 여부: {}", isLastDayOfMonth); log.info("월 말일 여부: {}", isLastDayOfMonth);
log.info("========================================"); log.info("========================================");
// Daily 파티션 처리 (매일) // 1. Daily 파티션 생성 (매일)
processDailyPartitions(today); createDailyPartitions(today);
// Monthly 파티션 처리 (매월 말일만) // 2. Daily 파티션 삭제 (보관기간 초과분)
deleteDailyPartitions(today);
// 3. Monthly 파티션 생성 (매월 말일만)
if (isLastDayOfMonth) { if (isLastDayOfMonth) {
processMonthlyPartitions(today); createMonthlyPartitions(today);
} else { } else {
log.info("Monthly 파티션: 말일이 아니므로 스킵"); log.info("Monthly 파티션 생성: 말일이 아니므로 스킵");
}
// 4. Monthly 파티션 삭제 (매월 1일에만, 보관기간 초과분)
if (today.getDayOfMonth() == 1) {
deleteMonthlyPartitions(today);
} else {
log.info("Monthly 파티션 삭제: 1일이 아니므로 스킵");
} }
log.info("========================================"); log.info("========================================");
@ -76,36 +104,38 @@ public class PartitionManagerTasklet implements Tasklet {
return date.getDayOfMonth() == YearMonth.from(date).lengthOfMonth(); return date.getDayOfMonth() == YearMonth.from(date).lengthOfMonth();
} }
/** // ==================== Daily 파티션 생성 ====================
* Daily 파티션 처리
*/
private void processDailyPartitions(LocalDate today) {
List<PartitionTableInfo> tables = partitionConfig.getDailyPartitionTables();
if (tables.isEmpty()) { /**
log.info("Daily 파티션: 대상 테이블 없음"); * Daily 파티션 생성
*/
private void createDailyPartitions(LocalDate today) {
List<PartitionTableConfig> tables = partitionConfig.getDailyTables();
if (tables == null || tables.isEmpty()) {
log.info("Daily 파티션 생성: 대상 테이블 없음");
return; return;
} }
log.info("Daily 파티션 처리 시작: {} 개 테이블", tables.size()); log.info("Daily 파티션 생성 시작: {} 개 테이블", tables.size());
for (PartitionTableInfo table : tables) { for (PartitionTableConfig table : tables) {
processDailyPartition(table, today); createDailyPartitionsForTable(table, today);
} }
} }
/** /**
* 개별 Daily 파티션 생성 * 개별 테이블 Daily 파티션 생성
*/ */
private void processDailyPartition(PartitionTableInfo table, LocalDate today) { private void createDailyPartitionsForTable(PartitionTableConfig table, LocalDate today) {
List<String> created = new ArrayList<>(); List<String> created = new ArrayList<>();
List<String> skipped = new ArrayList<>(); List<String> skipped = new ArrayList<>();
for (int i = 0; i <= table.periodsAhead(); i++) { for (int i = 0; i <= table.getPeriodsAhead(); i++) {
LocalDate targetDate = today.plusDays(i); LocalDate targetDate = today.plusDays(i);
String partitionName = getDailyPartitionName(table.tableName(), targetDate); String partitionName = getDailyPartitionName(table.getTableName(), targetDate);
if (partitionExists(table.schema(), partitionName)) { if (partitionExists(table.getSchema(), partitionName)) {
skipped.add(partitionName); skipped.add(partitionName);
} else { } else {
createDailyPartition(table, targetDate, partitionName); createDailyPartition(table, targetDate, partitionName);
@ -113,40 +143,97 @@ public class PartitionManagerTasklet implements Tasklet {
} }
} }
log.info("[{}] Daily 파티션 - 생성: {}, 스킵: {}", log.info("[{}] Daily 파티션 생성 - 생성: {}, 스킵: {}",
table.tableName(), created.size(), skipped.size()); table.getTableName(), created.size(), skipped.size());
if (!created.isEmpty()) {
log.info("[{}] 생성된 파티션: {}", table.getTableName(), created);
}
} }
/** // ==================== Daily 파티션 삭제 ====================
* Monthly 파티션 처리
*/
private void processMonthlyPartitions(LocalDate today) {
List<PartitionTableInfo> tables = partitionConfig.getMonthlyPartitionTables();
if (tables.isEmpty()) { /**
log.info("Monthly 파티션: 대상 테이블 없음"); * Daily 파티션 삭제 (보관기간 초과분)
*/
private void deleteDailyPartitions(LocalDate today) {
List<PartitionTableConfig> tables = partitionConfig.getDailyTables();
if (tables == null || tables.isEmpty()) {
log.info("Daily 파티션 삭제: 대상 테이블 없음");
return; return;
} }
log.info("Monthly 파티션 처리 시작: {} 개 테이블", tables.size()); log.info("Daily 파티션 삭제 시작: {} 개 테이블", tables.size());
for (PartitionTableInfo table : tables) { for (PartitionTableConfig table : tables) {
processMonthlyPartition(table, today); int retentionDays = partitionConfig.getDailyRetentionDays(table.getTableName());
deleteDailyPartitionsForTable(table, today, retentionDays);
} }
} }
/** /**
* 개별 Monthly 파티션 생성 * 개별 테이블 Daily 파티션 삭제
*/ */
private void processMonthlyPartition(PartitionTableInfo table, LocalDate today) { private void deleteDailyPartitionsForTable(PartitionTableConfig table, LocalDate today, int retentionDays) {
LocalDate cutoffDate = today.minusDays(retentionDays);
String likePattern = table.getTableName() + "_%";
List<String> partitions = jdbcTemplate.queryForList(
FIND_PARTITIONS_SQL, String.class, table.getSchema(), likePattern);
List<String> deleted = new ArrayList<>();
for (String partitionName : partitions) {
// 파티션 이름에서 날짜 추출 (table_YYMMDD)
LocalDate partitionDate = parseDailyPartitionDate(table.getTableName(), partitionName);
if (partitionDate != null && partitionDate.isBefore(cutoffDate)) {
dropPartition(table.getSchema(), partitionName);
deleted.add(partitionName);
}
}
if (!deleted.isEmpty()) {
log.info("[{}] Daily 파티션 삭제 - 보관기간: {}일, 삭제: {} 개",
table.getTableName(), retentionDays, deleted.size());
log.info("[{}] 삭제된 파티션: {}", table.getTableName(), deleted);
} else {
log.info("[{}] Daily 파티션 삭제 - 보관기간: {}일, 삭제할 파티션 없음",
table.getTableName(), retentionDays);
}
}
// ==================== Monthly 파티션 생성 ====================
/**
* Monthly 파티션 생성
*/
private void createMonthlyPartitions(LocalDate today) {
List<PartitionTableConfig> tables = partitionConfig.getMonthlyTables();
if (tables == null || tables.isEmpty()) {
log.info("Monthly 파티션 생성: 대상 테이블 없음");
return;
}
log.info("Monthly 파티션 생성 시작: {} 개 테이블", tables.size());
for (PartitionTableConfig table : tables) {
createMonthlyPartitionsForTable(table, today);
}
}
/**
* 개별 테이블 Monthly 파티션 생성
*/
private void createMonthlyPartitionsForTable(PartitionTableConfig table, LocalDate today) {
List<String> created = new ArrayList<>(); List<String> created = new ArrayList<>();
List<String> skipped = new ArrayList<>(); List<String> skipped = new ArrayList<>();
for (int i = 0; i <= table.periodsAhead(); i++) { for (int i = 0; i <= table.getPeriodsAhead(); i++) {
LocalDate targetDate = today.plusMonths(i).withDayOfMonth(1); LocalDate targetDate = today.plusMonths(i).withDayOfMonth(1);
String partitionName = getMonthlyPartitionName(table.tableName(), targetDate); String partitionName = getMonthlyPartitionName(table.getTableName(), targetDate);
if (partitionExists(table.schema(), partitionName)) { if (partitionExists(table.getSchema(), partitionName)) {
skipped.add(partitionName); skipped.add(partitionName);
} else { } else {
createMonthlyPartition(table, targetDate, partitionName); createMonthlyPartition(table, targetDate, partitionName);
@ -154,27 +241,127 @@ public class PartitionManagerTasklet implements Tasklet {
} }
} }
log.info("[{}] Monthly 파티션 - 생성: {}, 스킵: {}", log.info("[{}] Monthly 파티션 생성 - 생성: {}, 스킵: {}",
table.tableName(), created.size(), skipped.size()); table.getTableName(), created.size(), skipped.size());
if (!created.isEmpty()) { if (!created.isEmpty()) {
log.info("[{}] 생성된 파티션: {}", table.tableName(), created); log.info("[{}] 생성된 파티션: {}", table.getTableName(), created);
}
}
// ==================== Monthly 파티션 삭제 ====================
/**
* Monthly 파티션 삭제 (보관기간 초과분)
*/
private void deleteMonthlyPartitions(LocalDate today) {
List<PartitionTableConfig> tables = partitionConfig.getMonthlyTables();
if (tables == null || tables.isEmpty()) {
log.info("Monthly 파티션 삭제: 대상 테이블 없음");
return;
}
log.info("Monthly 파티션 삭제 시작: {} 개 테이블", tables.size());
for (PartitionTableConfig table : tables) {
int retentionMonths = partitionConfig.getMonthlyRetentionMonths(table.getTableName());
deleteMonthlyPartitionsForTable(table, today, retentionMonths);
} }
} }
/** /**
* Daily 파티션 이름 생성 (table_YYYY_MM_DD) * 개별 테이블 Monthly 파티션 삭제
*/
private void deleteMonthlyPartitionsForTable(PartitionTableConfig table, LocalDate today, int retentionMonths) {
LocalDate cutoffDate = today.minusMonths(retentionMonths).withDayOfMonth(1);
String likePattern = table.getTableName() + "_%";
List<String> partitions = jdbcTemplate.queryForList(
FIND_PARTITIONS_SQL, String.class, table.getSchema(), likePattern);
List<String> deleted = new ArrayList<>();
for (String partitionName : partitions) {
// 파티션 이름에서 날짜 추출 (table_YYYY_MM)
LocalDate partitionDate = parseMonthlyPartitionDate(table.getTableName(), partitionName);
if (partitionDate != null && partitionDate.isBefore(cutoffDate)) {
dropPartition(table.getSchema(), partitionName);
deleted.add(partitionName);
}
}
if (!deleted.isEmpty()) {
log.info("[{}] Monthly 파티션 삭제 - 보관기간: {}개월, 삭제: {} 개",
table.getTableName(), retentionMonths, deleted.size());
log.info("[{}] 삭제된 파티션: {}", table.getTableName(), deleted);
} else {
log.info("[{}] Monthly 파티션 삭제 - 보관기간: {}개월, 삭제할 파티션 없음",
table.getTableName(), retentionMonths);
}
}
// ==================== 파티션 이름 생성 ====================
/**
* Daily 파티션 이름 생성 (table_YYMMDD)
*/ */
private String getDailyPartitionName(String tableName, LocalDate date) { private String getDailyPartitionName(String tableName, LocalDate date) {
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyy_MM_dd")); return tableName + "_" + date.format(DAILY_PARTITION_FORMAT);
} }
/** /**
* Monthly 파티션 이름 생성 (table_YYYY_MM) * Monthly 파티션 이름 생성 (table_YYYY_MM)
*/ */
private String getMonthlyPartitionName(String tableName, LocalDate date) { private String getMonthlyPartitionName(String tableName, LocalDate date) {
return tableName + "_" + date.format(DateTimeFormatter.ofPattern("yyyy_MM")); return tableName + "_" + date.format(MONTHLY_PARTITION_FORMAT);
} }
// ==================== 파티션 이름에서 날짜 추출 ====================
/**
* Daily 파티션 이름에서 날짜 추출 (table_YYMMDD -> LocalDate)
*/
private LocalDate parseDailyPartitionDate(String tableName, String partitionName) {
try {
String prefix = tableName + "_";
if (!partitionName.startsWith(prefix)) {
return null;
}
String dateStr = partitionName.substring(prefix.length());
// YYMMDD 형식 (6자리)
if (dateStr.length() == 6 && dateStr.matches("\\d{6}")) {
return LocalDate.parse(dateStr, DAILY_PARTITION_FORMAT);
}
return null;
} catch (Exception e) {
log.trace("파티션 날짜 파싱 실패: {}", partitionName);
return null;
}
}
/**
* Monthly 파티션 이름에서 날짜 추출 (table_YYYY_MM -> LocalDate)
*/
private LocalDate parseMonthlyPartitionDate(String tableName, String partitionName) {
try {
String prefix = tableName + "_";
if (!partitionName.startsWith(prefix)) {
return null;
}
String dateStr = partitionName.substring(prefix.length());
// YYYY_MM 형식 (7자리)
if (dateStr.length() == 7 && dateStr.matches("\\d{4}_\\d{2}")) {
return LocalDate.parse(dateStr + "_01", DateTimeFormatter.ofPattern("yyyy_MM_dd"));
}
return null;
} catch (Exception e) {
log.trace("파티션 날짜 파싱 실패: {}", partitionName);
return null;
}
}
// ==================== DB 작업 ====================
/** /**
* 파티션 존재 여부 확인 * 파티션 존재 여부 확인
*/ */
@ -186,14 +373,14 @@ public class PartitionManagerTasklet implements Tasklet {
/** /**
* Daily 파티션 생성 * Daily 파티션 생성
*/ */
private void createDailyPartition(PartitionTableInfo table, LocalDate targetDate, String partitionName) { private void createDailyPartition(PartitionTableConfig table, LocalDate targetDate, String partitionName) {
LocalDate endDate = targetDate.plusDays(1); LocalDate endDate = targetDate.plusDays(1);
String sql = String.format(""" String sql = String.format("""
CREATE TABLE %s.%s PARTITION OF %s CREATE TABLE %s.%s PARTITION OF %s
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00') FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
""", """,
table.schema(), partitionName, table.getFullTableName(), table.getSchema(), partitionName, table.getFullTableName(),
targetDate, endDate); targetDate, endDate);
jdbcTemplate.execute(sql); jdbcTemplate.execute(sql);
@ -203,7 +390,7 @@ public class PartitionManagerTasklet implements Tasklet {
/** /**
* Monthly 파티션 생성 * Monthly 파티션 생성
*/ */
private void createMonthlyPartition(PartitionTableInfo table, LocalDate targetDate, String partitionName) { private void createMonthlyPartition(PartitionTableConfig table, LocalDate targetDate, String partitionName) {
LocalDate startDate = targetDate.withDayOfMonth(1); LocalDate startDate = targetDate.withDayOfMonth(1);
LocalDate endDate = startDate.plusMonths(1); LocalDate endDate = startDate.plusMonths(1);
@ -211,10 +398,19 @@ public class PartitionManagerTasklet implements Tasklet {
CREATE TABLE %s.%s PARTITION OF %s CREATE TABLE %s.%s PARTITION OF %s
FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00') FOR VALUES FROM ('%s 00:00:00+00') TO ('%s 00:00:00+00')
""", """,
table.schema(), partitionName, table.getFullTableName(), table.getSchema(), partitionName, table.getFullTableName(),
startDate, endDate); startDate, endDate);
jdbcTemplate.execute(sql); jdbcTemplate.execute(sql);
log.debug("Monthly 파티션 생성: {}", partitionName); log.debug("Monthly 파티션 생성: {}", partitionName);
} }
/**
* 파티션 삭제
*/
private void dropPartition(String schema, String partitionName) {
String sql = String.format("DROP TABLE IF EXISTS %s.%s", schema, partitionName);
jdbcTemplate.execute(sql);
log.debug("파티션 삭제: {}", partitionName);
}
} }

파일 보기

@ -46,7 +46,7 @@ public class AisTargetRepositoryImpl implements AisTargetRepository {
received_date, collected_at, created_at, updated_at received_date, collected_at, created_at, updated_at
) VALUES ( ) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ST_SetSRID(ST_MakePoint(?, ?), 4326), ?, ?, public.ST_SetSRID(public.ST_MakePoint(?, ?), 4326),
?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
@ -203,9 +203,9 @@ public class AisTargetRepositoryImpl implements AisTargetRepository {
SELECT DISTINCT ON (mmsi) * SELECT DISTINCT ON (mmsi) *
FROM %s FROM %s
WHERE message_timestamp BETWEEN ? AND ? WHERE message_timestamp BETWEEN ? AND ?
AND ST_DWithin( AND public.ST_DWithin(
geom::geography, geom::geography,
ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, public.ST_SetSRID(public.ST_MakePoint(?, ?), 4326)::geography,
? ?
) )
ORDER BY mmsi, message_timestamp DESC ORDER BY mmsi, message_timestamp DESC

파일 보기

@ -4,7 +4,7 @@ spring:
# PostgreSQL Database Configuration # PostgreSQL Database Configuration
datasource: datasource:
url: jdbc:postgresql://10.187.58.58:5432/mdadb?currentSchema=snp_data url: jdbc:postgresql://10.187.58.58:5432/mdadb?currentSchema=snp_data,public
username: mda username: mda
password: mda#8932 password: mda#8932
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
@ -28,7 +28,7 @@ spring:
batch: batch:
jdbc: jdbc:
table-prefix: "snp_data.batch_" table-prefix: "snp_data.batch_"
initialize-schema: always # Changed to 'never' as tables already exist initialize-schema: never # Changed to 'never' as tables already exist
job: job:
enabled: false # Prevent auto-run on startup enabled: false # Prevent auto-run on startup
@ -42,7 +42,7 @@ spring:
quartz: quartz:
job-store-type: jdbc # JDBC store for schedule persistence job-store-type: jdbc # JDBC store for schedule persistence
jdbc: jdbc:
initialize-schema: always # Create Quartz tables if not exist initialize-schema: never # Create Quartz tables if not exist
properties: properties:
org.quartz.scheduler.instanceName: SNPBatchScheduler org.quartz.scheduler.instanceName: SNPBatchScheduler
org.quartz.scheduler.instanceId: AUTO org.quartz.scheduler.instanceId: AUTO
@ -55,9 +55,9 @@ spring:
# Server Configuration # Server Configuration
server: server:
port: 8041 port: 9000
servlet: servlet:
context-path: / context-path: /snp-api
# Actuator Configuration # Actuator Configuration
management: management:
@ -69,18 +69,10 @@ management:
health: health:
show-details: always show-details: always
# Logging Configuration
# Logging Configuration (logback-spring.xml에서 상세 설정)
logging: logging:
level: config: classpath:logback-spring.xml
root: INFO
com.snp.batch: DEBUG
org.springframework.batch: DEBUG
org.springframework.jdbc: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/snp-batch.log
# Custom Application Properties # Custom Application Properties
app: app:

파일 보기

@ -91,15 +91,33 @@ app:
schedule: schedule:
enabled: true enabled: true
cron: "0 0 * * * ?" # Every hour cron: "0 0 * * * ?" # Every hour
# AIS Target 배치 설정 # AIS Target 배치 설정
ais-target: ais-target:
since-seconds: 60 # API 조회 범위 (초) since-seconds: 60 # API 조회 범위 (초)
chunk-size: 5000 # 배치 청크 크기 chunk-size: 5000 # 배치 청크 크기
schedule: schedule:
cron: "15 * * * * ?" # 매 분 15초 실행 cron: "15 * * * * ?" # 매 분 15초 실행
partition:
months-ahead: 2 # 미리 생성할 파티션 개월 수
# AIS Target 캐시 설정 # AIS Target 캐시 설정
ais-target-cache: ais-target-cache:
ttl-minutes: 120 # 캐시 TTL (분) - 2시간 ttl-minutes: 120 # 캐시 TTL (분) - 2시간
max-size: 300000 # 최대 캐시 크기 - 30만 건 max-size: 300000 # 최대 캐시 크기 - 30만 건
# 파티션 관리 설정
partition:
# 일별 파티션 테이블 목록 (네이밍: {table}_YYMMDD)
daily-tables:
- schema: snp_data
table-name: ais_target
partition-column: message_timestamp
periods-ahead: 3 # 미리 생성할 일수
# 월별 파티션 테이블 목록 (네이밍: {table}_YYYY_MM)
monthly-tables: [] # 현재 없음
# 기본 보관기간
retention:
daily-default-days: 14 # 일별 파티션 기본 보관기간 (14일)
monthly-default-months: 1 # 월별 파티션 기본 보관기간 (1개월)
# 개별 테이블 보관기간 설정 (옵션)
custom:
# - table-name: ais_target
# retention-days: 30 # ais_target만 30일 보관

파일 보기

@ -3,8 +3,8 @@
-- ============================================ -- ============================================
-- 용도: 선박 AIS 위치 정보 저장 (항적 분석용) -- 용도: 선박 AIS 위치 정보 저장 (항적 분석용)
-- 수집 주기: 매 분 15초 -- 수집 주기: 매 분 15초
-- 예상 데이터량: 약 33,000건/분 -- 예상 데이터량: 약 33,000건/분, 일 20GB (인덱스 포함)
-- 파티셔닝: 월별 파티션 (ais_target_YYYY_MM) -- 파티셔닝: 일별 파티션 (ais_target_YYMMDD)
-- ============================================ -- ============================================
-- PostGIS 확장 활성화 (이미 설치되어 있다면 생략) -- PostGIS 확장 활성화 (이미 설치되어 있다면 생략)
@ -77,18 +77,26 @@ CREATE TABLE IF NOT EXISTS snp_data.ais_target (
) PARTITION BY RANGE (message_timestamp); ) PARTITION BY RANGE (message_timestamp);
-- ============================================ -- ============================================
-- 2. 초기 파티션 생성 (현재 월 + 다음 월) -- 2. 초기 파티션 생성 (현재 일 + 다음 3일)
-- ============================================ -- ============================================
-- 예: 2025년 12월과 2026년 1월 파티션 -- 파티션 네이밍: ais_target_YYMMDD
-- 실제 운영 시 create_ais_target_partition 함수로 자동 생성 -- 실제 운영 시 partitionManagerJob에서 자동 생성
-- 2025년 12월 파티션 -- 2024년 12월 4일 파티션 (예시)
CREATE TABLE IF NOT EXISTS snp_data.ais_target_2025_12 PARTITION OF snp_data.ais_target CREATE TABLE IF NOT EXISTS snp_data.ais_target_241204 PARTITION OF snp_data.ais_target
FOR VALUES FROM ('2025-12-01 00:00:00+00') TO ('2026-01-01 00:00:00+00'); FOR VALUES FROM ('2024-12-04 00:00:00+00') TO ('2024-12-05 00:00:00+00');
-- 2026년 1월 파티션 -- 2024년 12월 5일 파티션
CREATE TABLE IF NOT EXISTS snp_data.ais_target_2026_01 PARTITION OF snp_data.ais_target CREATE TABLE IF NOT EXISTS snp_data.ais_target_241205 PARTITION OF snp_data.ais_target
FOR VALUES FROM ('2026-01-01 00:00:00+00') TO ('2026-02-01 00:00:00+00'); FOR VALUES FROM ('2024-12-05 00:00:00+00') TO ('2024-12-06 00:00:00+00');
-- 2024년 12월 6일 파티션
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241206 PARTITION OF snp_data.ais_target
FOR VALUES FROM ('2024-12-06 00:00:00+00') TO ('2024-12-07 00:00:00+00');
-- 2024년 12월 7일 파티션
CREATE TABLE IF NOT EXISTS snp_data.ais_target_241207 PARTITION OF snp_data.ais_target
FOR VALUES FROM ('2024-12-07 00:00:00+00') TO ('2024-12-08 00:00:00+00');
-- ============================================ -- ============================================
-- 3. 인덱스 생성 (각 파티션에 자동 상속) -- 3. 인덱스 생성 (각 파티션에 자동 상속)
@ -120,7 +128,7 @@ CREATE INDEX IF NOT EXISTS idx_ais_target_collected_at
ON snp_data.ais_target (collected_at DESC); ON snp_data.ais_target (collected_at DESC);
-- ============================================ -- ============================================
-- 4. 파티션 자동 생성 함수 -- 4. 파티션 자동 생성 함수 (일별)
-- ============================================ -- ============================================
-- 파티션 존재 여부 확인 함수 -- 파티션 존재 여부 확인 함수
@ -137,8 +145,8 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- 특정 의 파티션 생성 함수 -- 특정 의 파티션 생성 함수
CREATE OR REPLACE FUNCTION snp_data.create_ais_target_partition(target_date DATE) CREATE OR REPLACE FUNCTION snp_data.create_ais_target_daily_partition(target_date DATE)
RETURNS TEXT AS $$ RETURNS TEXT AS $$
DECLARE DECLARE
partition_name TEXT; partition_name TEXT;
@ -146,12 +154,12 @@ DECLARE
end_date DATE; end_date DATE;
create_sql TEXT; create_sql TEXT;
BEGIN BEGIN
-- 파티션 이름 생성: ais_target_YYYY_MM -- 파티션 이름 생성: ais_target_YYMMDD
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM'); partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
-- 시작/종료 날짜 계산 -- 시작/종료 날짜 계산
start_date := DATE_TRUNC('month', target_date)::DATE; start_date := target_date;
end_date := (DATE_TRUNC('month', target_date) + INTERVAL '1 month')::DATE; end_date := target_date + INTERVAL '1 day';
-- 이미 존재하면 스킵 -- 이미 존재하면 스킵
IF snp_data.partition_exists(partition_name) THEN IF snp_data.partition_exists(partition_name) THEN
@ -175,18 +183,18 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- 다음 N개월 파티션 사전 생성 함수 -- 다음 N 파티션 사전 생성 함수
CREATE OR REPLACE FUNCTION snp_data.create_future_ais_target_partitions(months_ahead INTEGER DEFAULT 2) CREATE OR REPLACE FUNCTION snp_data.create_future_ais_target_daily_partitions(days_ahead INTEGER DEFAULT 3)
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$ RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
DECLARE DECLARE
i INTEGER; i INTEGER;
target_date DATE; target_date DATE;
result TEXT; result TEXT;
BEGIN BEGIN
FOR i IN 0..months_ahead LOOP FOR i IN 0..days_ahead LOOP
target_date := DATE_TRUNC('month', CURRENT_DATE + (i || ' months')::INTERVAL)::DATE; target_date := CURRENT_DATE + i;
result := snp_data.create_ais_target_partition(target_date); result := snp_data.create_ais_target_daily_partition(target_date);
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM'); partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
status := result; status := result;
RETURN NEXT; RETURN NEXT;
END LOOP; END LOOP;
@ -194,17 +202,17 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- ============================================ -- ============================================
-- 5. 오래된 파티션 삭제 함수 -- 5. 오래된 파티션 삭제 함수 (일별)
-- ============================================ -- ============================================
-- 특정 의 파티션 삭제 함수 -- 특정 의 파티션 삭제 함수
CREATE OR REPLACE FUNCTION snp_data.drop_ais_target_partition(target_date DATE) CREATE OR REPLACE FUNCTION snp_data.drop_ais_target_daily_partition(target_date DATE)
RETURNS TEXT AS $$ RETURNS TEXT AS $$
DECLARE DECLARE
partition_name TEXT; partition_name TEXT;
drop_sql TEXT; drop_sql TEXT;
BEGIN BEGIN
partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYYY_MM'); partition_name := 'ais_target_' || TO_CHAR(target_date, 'YYMMDD');
-- 존재하지 않으면 스킵 -- 존재하지 않으면 스킵
IF NOT snp_data.partition_exists(partition_name) THEN IF NOT snp_data.partition_exists(partition_name) THEN
@ -221,17 +229,17 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- N개월 이전 파티션 정리 함수 -- N 이전 파티션 정리 함수
CREATE OR REPLACE FUNCTION snp_data.cleanup_old_ais_target_partitions(retention_months INTEGER DEFAULT 3) CREATE OR REPLACE FUNCTION snp_data.cleanup_old_ais_target_daily_partitions(retention_days INTEGER DEFAULT 14)
RETURNS TABLE (partition_name TEXT, status TEXT) AS $$ RETURNS TABLE (partition_name TEXT, status TEXT) AS $$
DECLARE DECLARE
rec RECORD; rec RECORD;
partition_date DATE; partition_date DATE;
cutoff_date DATE; cutoff_date DATE;
BEGIN BEGIN
cutoff_date := DATE_TRUNC('month', CURRENT_DATE - (retention_months || ' months')::INTERVAL)::DATE; cutoff_date := CURRENT_DATE - retention_days;
-- ais_target_YYYY_MM 패턴의 파티션 조회 -- ais_target_YYMMDD 패턴의 파티션 조회
FOR rec IN FOR rec IN
SELECT c.relname SELECT c.relname
FROM pg_class c FROM pg_class c
@ -239,12 +247,13 @@ BEGIN
JOIN pg_inherits i ON i.inhrelid = c.oid JOIN pg_inherits i ON i.inhrelid = c.oid
WHERE n.nspname = 'snp_data' WHERE n.nspname = 'snp_data'
AND c.relname LIKE 'ais_target_%' AND c.relname LIKE 'ais_target_%'
AND LENGTH(c.relname) = 17 -- ais_target_YYMMDD = 17자
AND c.relkind = 'r' AND c.relkind = 'r'
ORDER BY c.relname ORDER BY c.relname
LOOP LOOP
-- 파티션 이름에서 날짜 추출 (ais_target_YYYY_MM) -- 파티션 이름에서 날짜 추출 (ais_target_YYMMDD)
BEGIN BEGIN
partition_date := TO_DATE(SUBSTRING(rec.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM'); partition_date := TO_DATE(SUBSTRING(rec.relname FROM 'ais_target_(\d{6})'), 'YYMMDD');
IF partition_date < cutoff_date THEN IF partition_date < cutoff_date THEN
EXECUTE format('DROP TABLE snp_data.%I', rec.relname); EXECUTE format('DROP TABLE snp_data.%I', rec.relname);
@ -261,8 +270,8 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- 파티션별 통계 조회 함수 -- 파티션별 통계 조회 함수 (일별)
CREATE OR REPLACE FUNCTION snp_data.ais_target_partition_stats() CREATE OR REPLACE FUNCTION snp_data.ais_target_daily_partition_stats()
RETURNS TABLE ( RETURNS TABLE (
partition_name TEXT, partition_name TEXT,
row_count BIGINT, row_count BIGINT,
@ -273,11 +282,7 @@ BEGIN
RETURN QUERY RETURN QUERY
SELECT SELECT
c.relname::TEXT as partition_name, c.relname::TEXT as partition_name,
(SELECT COUNT(*)::BIGINT FROM snp_data.ais_target WHERE message_timestamp >= (pg_stat_get_live_tuples(c.oid))::BIGINT as row_count,
TO_DATE(SUBSTRING(c.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM')
AND message_timestamp <
TO_DATE(SUBSTRING(c.relname FROM 'ais_target_(\d{4}_\d{2})'), 'YYYY_MM') + INTERVAL '1 month'
) as row_count,
pg_relation_size(c.oid) as size_bytes, pg_relation_size(c.oid) as size_bytes,
pg_size_pretty(pg_relation_size(c.oid)) as size_pretty pg_size_pretty(pg_relation_size(c.oid)) as size_pretty
FROM pg_class c FROM pg_class c
@ -294,7 +299,7 @@ $$ LANGUAGE plpgsql;
-- 6. 코멘트 -- 6. 코멘트
-- ============================================ -- ============================================
COMMENT ON TABLE snp_data.ais_target IS 'AIS 선박 위치 정보 (매 분 15초 수집, 월별 파티션)'; COMMENT ON TABLE snp_data.ais_target IS 'AIS 선박 위치 정보 (매 분 15초 수집, 일별 파티션 - ais_target_YYMMDD)';
COMMENT ON COLUMN snp_data.ais_target.mmsi IS 'Maritime Mobile Service Identity (복합 PK)'; COMMENT ON COLUMN snp_data.ais_target.mmsi IS 'Maritime Mobile Service Identity (복합 PK)';
COMMENT ON COLUMN snp_data.ais_target.message_timestamp IS 'AIS 메시지 발생 시간 (복합 PK, 파티션 키)'; COMMENT ON COLUMN snp_data.ais_target.message_timestamp IS 'AIS 메시지 발생 시간 (복합 PK, 파티션 키)';
@ -308,33 +313,6 @@ COMMENT ON COLUMN snp_data.ais_target.draught IS '흘수 (meters)';
COMMENT ON COLUMN snp_data.ais_target.collected_at IS '배치 수집 시점'; COMMENT ON COLUMN snp_data.ais_target.collected_at IS '배치 수집 시점';
COMMENT ON COLUMN snp_data.ais_target.received_date IS 'API 수신 시간'; COMMENT ON COLUMN snp_data.ais_target.received_date IS 'API 수신 시간';
-- ============================================
-- 유지보수용 함수: 오래된 데이터 정리
-- ============================================
-- 오래된 데이터 삭제 함수 (기본: 7일 이전)
CREATE OR REPLACE FUNCTION snp_data.cleanup_ais_target(retention_days INTEGER DEFAULT 7)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM snp_data.ais_target
WHERE message_timestamp < NOW() - (retention_days || ' days')::INTERVAL;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RAISE NOTICE 'Deleted % rows older than % days', deleted_count, retention_days;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION snp_data.create_ais_target_partition IS '특정 월의 AIS Target 파티션 생성';
COMMENT ON FUNCTION snp_data.create_future_ais_target_partitions IS '향후 N개월 파티션 사전 생성';
COMMENT ON FUNCTION snp_data.drop_ais_target_partition IS '특정 월의 파티션 삭제';
COMMENT ON FUNCTION snp_data.cleanup_old_ais_target_partitions IS 'N개월 이전 파티션 정리';
COMMENT ON FUNCTION snp_data.ais_target_partition_stats IS '파티션별 통계 조회';
-- ============================================ -- ============================================
-- 7. 유지보수용 함수: 통계 조회 -- 7. 유지보수용 함수: 통계 조회
-- ============================================ -- ============================================
@ -362,6 +340,11 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계 조회'; COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계 조회';
COMMENT ON FUNCTION snp_data.create_ais_target_daily_partition IS '특정 일의 AIS Target 파티션 생성';
COMMENT ON FUNCTION snp_data.create_future_ais_target_daily_partitions IS '향후 N일 파티션 사전 생성';
COMMENT ON FUNCTION snp_data.drop_ais_target_daily_partition IS '특정 일의 파티션 삭제';
COMMENT ON FUNCTION snp_data.cleanup_old_ais_target_daily_partitions IS 'N일 이전 파티션 정리';
COMMENT ON FUNCTION snp_data.ais_target_daily_partition_stats IS '파티션별 통계 조회';
-- ============================================ -- ============================================
-- 예시 쿼리 -- 예시 쿼리
@ -373,7 +356,7 @@ COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계
-- 2. 특정 시간 범위의 항적 조회 -- 2. 특정 시간 범위의 항적 조회
-- SELECT * FROM snp_data.ais_target -- SELECT * FROM snp_data.ais_target
-- WHERE mmsi = 123456789 -- WHERE mmsi = 123456789
-- AND message_timestamp BETWEEN '2025-12-01 00:00:00+00' AND '2025-12-01 01:00:00+00' -- AND message_timestamp BETWEEN '2024-12-04 00:00:00+00' AND '2024-12-04 01:00:00+00'
-- ORDER BY message_timestamp; -- ORDER BY message_timestamp;
-- 3. 특정 구역(원형) 내 선박 조회 -- 3. 특정 구역(원형) 내 선박 조회
@ -387,26 +370,19 @@ COMMENT ON FUNCTION snp_data.ais_target_stats IS 'AIS Target 테이블 통계
-- ) -- )
-- ORDER BY mmsi, message_timestamp DESC; -- ORDER BY mmsi, message_timestamp DESC;
-- 4. LineString 항적 생성 -- 4. 다음 7일 파티션 미리 생성
-- SELECT mmsi, ST_MakeLine(geom ORDER BY message_timestamp) as track -- SELECT * FROM snp_data.create_future_ais_target_daily_partitions(7);
-- FROM snp_data.ais_target
-- WHERE mmsi = 123456789
-- AND message_timestamp BETWEEN '2025-12-01 00:00:00+00' AND '2025-12-01 01:00:00+00'
-- GROUP BY mmsi;
-- 5. 다음 3개월 파티션 미리 생성 -- 5. 특정 일 파티션 생성
-- SELECT * FROM snp_data.create_future_ais_target_partitions(3); -- SELECT snp_data.create_ais_target_daily_partition('2024-12-10');
-- 6. 특정 월 파티션 생성 -- 6. 14일 이전 파티션 정리
-- SELECT snp_data.create_ais_target_partition('2026-03-01'); -- SELECT * FROM snp_data.cleanup_old_ais_target_daily_partitions(14);
-- 7. 3개월 이전 파티션 정리 -- 7. 파티션별 통계 조회
-- SELECT * FROM snp_data.cleanup_old_ais_target_partitions(3); -- SELECT * FROM snp_data.ais_target_daily_partition_stats();
-- 8. 파티션별 통계 조회 -- 8. 전체 통계 조회
-- SELECT * FROM snp_data.ais_target_partition_stats();
-- 9. 전체 통계 조회
-- SELECT * FROM snp_data.ais_target_stats(); -- SELECT * FROM snp_data.ais_target_stats();
-- ============================================ -- ============================================
@ -431,12 +407,13 @@ VALUES (
updated_at = NOW(); updated_at = NOW();
-- 2. partitionManagerJob: 매일 00:10에 실행 -- 2. partitionManagerJob: 매일 00:10에 실행
-- Daily 파티션: 매일 생성, Monthly 파티션: 말일에만 생성 (Job 내부에서 분기) -- Daily 파티션: 매일 생성/삭제 (ais_target_YYMMDD)
-- Monthly 파티션: 말일 생성, 1일 삭제 (table_YYYY_MM)
INSERT INTO public.job_schedule (job_name, cron_expression, description, active, created_at, updated_at, created_by, updated_by) INSERT INTO public.job_schedule (job_name, cron_expression, description, active, created_at, updated_at, created_by, updated_by)
VALUES ( VALUES (
'partitionManagerJob', 'partitionManagerJob',
'0 10 0 * * ?', '0 10 0 * * ?',
'파티션 관리 - 매일 00:10 실행 (Daily: 매일, Monthly: 말일만)', '파티션 관리 - 매일 00:10 실행 (Daily: 생성/삭제, Monthly: 말일 생성/1일 삭제)',
true, true,
NOW(), NOW(),
NOW(), NOW(),