Geojson File Monitoring System Created - Daniel C No.1
This commit is contained in:
@@ -2,8 +2,10 @@ package com.kamco.cd.kamcoback;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class KamcoBackApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.kamco.cd.kamcoback.geojson.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* GeoJSON 파일 모니터링 설정
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "geojson.monitor")
|
||||
@Getter
|
||||
@Setter
|
||||
public class GeoJsonMonitorConfig {
|
||||
|
||||
/**
|
||||
* 모니터링할 폴더 경로
|
||||
*/
|
||||
private String watchDirectory = "~/geojson/upload";
|
||||
|
||||
/**
|
||||
* 처리 완료 후 파일을 이동할 폴더 경로
|
||||
*/
|
||||
private String processedDirectory = "~/geojson/processed";
|
||||
|
||||
/**
|
||||
* 처리 실패 파일을 이동할 폴더 경로
|
||||
*/
|
||||
private String errorDirectory = "~/geojson/error";
|
||||
|
||||
/**
|
||||
* 파일 모니터링 스케줄 (cron 표현식)
|
||||
* 기본값: 매 30초마다 실행
|
||||
*/
|
||||
private String cronExpression = "0/30 * * * * *";
|
||||
|
||||
/**
|
||||
* 지원하는 압축파일 확장자
|
||||
*/
|
||||
private String[] supportedExtensions = {"zip", "tar", "tar.gz", "tgz"};
|
||||
|
||||
/**
|
||||
* 처리할 최대 파일 크기 (바이트)
|
||||
*/
|
||||
private long maxFileSize = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
/**
|
||||
* 임시 압축해제 폴더
|
||||
*/
|
||||
private String tempDirectory = "/tmp/geojson_extract";
|
||||
|
||||
/**
|
||||
* 홈 디렉토리 경로 확장
|
||||
*/
|
||||
@PostConstruct
|
||||
public void expandPaths() {
|
||||
watchDirectory = expandPath(watchDirectory);
|
||||
processedDirectory = expandPath(processedDirectory);
|
||||
errorDirectory = expandPath(errorDirectory);
|
||||
tempDirectory = expandPath(tempDirectory);
|
||||
}
|
||||
|
||||
private String expandPath(String path) {
|
||||
if (path.startsWith("~")) {
|
||||
return path.replace("~", System.getProperty("user.home"));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.kamco.cd.kamcoback.geojson.controller;
|
||||
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataGeomEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataGeomRepository;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* GeoJSON 데이터 조회 및 테스트용 API 컨트롤러
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/geojson/data")
|
||||
@RequiredArgsConstructor
|
||||
public class GeoJsonDataController {
|
||||
|
||||
private final MapSheetLearnDataRepository mapSheetLearnDataRepository;
|
||||
private final MapSheetLearnDataGeomRepository mapSheetLearnDataGeomRepository;
|
||||
|
||||
/**
|
||||
* 학습 데이터 목록 조회
|
||||
*/
|
||||
@GetMapping("/learn-data")
|
||||
public ResponseEntity<Map<String, Object>> getLearnDataList(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) String dataState,
|
||||
@RequestParam(required = false) String analState) {
|
||||
try {
|
||||
PageRequest pageRequest = PageRequest.of(page, size);
|
||||
List<MapSheetLearnDataEntity> learnDataList;
|
||||
|
||||
if (dataState != null) {
|
||||
learnDataList = mapSheetLearnDataRepository.findByDataState(dataState);
|
||||
} else if (analState != null) {
|
||||
learnDataList = mapSheetLearnDataRepository.findByAnalState(analState);
|
||||
} else {
|
||||
learnDataList = mapSheetLearnDataRepository.findAll(pageRequest).getContent();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("data", learnDataList);
|
||||
response.put("totalCount", learnDataList.size());
|
||||
response.put("page", page);
|
||||
response.put("size", size);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("학습 데이터 목록 조회 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "데이터 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 학습 데이터 상세 조회
|
||||
*/
|
||||
@GetMapping("/learn-data/{id}")
|
||||
public ResponseEntity<Map<String, Object>> getLearnDataDetail(@PathVariable Long id) {
|
||||
try {
|
||||
if (id == null) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "ID가 필요합니다."));
|
||||
}
|
||||
|
||||
Optional<MapSheetLearnDataEntity> learnDataOpt = mapSheetLearnDataRepository.findById(id);
|
||||
|
||||
if (learnDataOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
MapSheetLearnDataEntity learnData = learnDataOpt.get();
|
||||
List<MapSheetLearnDataGeomEntity> geometryList = mapSheetLearnDataGeomRepository.findByDataUid(id);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("learnData", learnData);
|
||||
response.put("geometryData", geometryList);
|
||||
response.put("geometryCount", geometryList.size());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("학습 데이터 상세 조회 실패: {}", id, e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "데이터 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Geometry 데이터 목록 조회
|
||||
*/
|
||||
@GetMapping("/geometry")
|
||||
public ResponseEntity<Map<String, Object>> getGeometryDataList(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(required = false) Long dataUid,
|
||||
@RequestParam(required = false) String geoType) {
|
||||
try {
|
||||
List<MapSheetLearnDataGeomEntity> geometryList;
|
||||
|
||||
if (dataUid != null) {
|
||||
geometryList = mapSheetLearnDataGeomRepository.findByDataUid(dataUid);
|
||||
} else if (geoType != null) {
|
||||
geometryList = mapSheetLearnDataGeomRepository.findByGeoType(geoType);
|
||||
} else {
|
||||
PageRequest pageRequest = PageRequest.of(page, size);
|
||||
geometryList = mapSheetLearnDataGeomRepository.findAll(pageRequest).getContent();
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("data", geometryList);
|
||||
response.put("totalCount", geometryList.size());
|
||||
response.put("page", page);
|
||||
response.put("size", size);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("Geometry 데이터 목록 조회 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "데이터 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 통계 정보 조회
|
||||
*/
|
||||
@GetMapping("/statistics")
|
||||
public ResponseEntity<Map<String, Object>> getStatistics() {
|
||||
try {
|
||||
long totalLearnData = mapSheetLearnDataRepository.count();
|
||||
long totalGeometryData = mapSheetLearnDataGeomRepository.count();
|
||||
|
||||
List<MapSheetLearnDataEntity> processedData = mapSheetLearnDataRepository.findByDataState("PROCESSED");
|
||||
List<MapSheetLearnDataEntity> pendingAnalysis = mapSheetLearnDataRepository.findByAnalState("PENDING");
|
||||
List<MapSheetLearnDataEntity> completedAnalysis = mapSheetLearnDataRepository.findByAnalState("COMPLETED");
|
||||
List<MapSheetLearnDataEntity> errorAnalysis = mapSheetLearnDataRepository.findByAnalState("ERROR");
|
||||
|
||||
Map<String, Object> statistics = new HashMap<>();
|
||||
statistics.put("totalLearnData", totalLearnData);
|
||||
statistics.put("totalGeometryData", totalGeometryData);
|
||||
statistics.put("processedDataCount", processedData.size());
|
||||
statistics.put("pendingAnalysisCount", pendingAnalysis.size());
|
||||
statistics.put("completedAnalysisCount", completedAnalysis.size());
|
||||
statistics.put("errorAnalysisCount", errorAnalysis.size());
|
||||
|
||||
// 처리 완료율 계산
|
||||
if (totalLearnData > 0) {
|
||||
double completionRate = (double) completedAnalysis.size() / totalLearnData * 100;
|
||||
statistics.put("completionRate", Math.round(completionRate * 100.0) / 100.0);
|
||||
} else {
|
||||
statistics.put("completionRate", 0.0);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"statistics", statistics,
|
||||
"timestamp", java.time.Instant.now()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("통계 정보 조회 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "통계 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 상태별 카운트 조회
|
||||
*/
|
||||
@GetMapping("/status-counts")
|
||||
public ResponseEntity<Map<String, Object>> getStatusCounts() {
|
||||
try {
|
||||
Map<String, Long> dataStateCounts = new HashMap<>();
|
||||
Map<String, Long> analStateCounts = new HashMap<>();
|
||||
|
||||
// 데이터 상태별 카운트
|
||||
dataStateCounts.put("PROCESSED", mapSheetLearnDataRepository.findByDataState("PROCESSED").size() + 0L);
|
||||
dataStateCounts.put("PENDING", mapSheetLearnDataRepository.findByDataStateIsNullOrDataState("PENDING").size() + 0L);
|
||||
|
||||
// 분석 상태별 카운트
|
||||
analStateCounts.put("PENDING", mapSheetLearnDataRepository.findByAnalState("PENDING").size() + 0L);
|
||||
analStateCounts.put("COMPLETED", mapSheetLearnDataRepository.findByAnalState("COMPLETED").size() + 0L);
|
||||
analStateCounts.put("ERROR", mapSheetLearnDataRepository.findByAnalState("ERROR").size() + 0L);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"dataStateCounts", dataStateCounts,
|
||||
"analStateCounts", analStateCounts,
|
||||
"timestamp", java.time.Instant.now()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("상태별 카운트 조회 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "카운트 조회 실패: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.kamco.cd.kamcoback.geojson.controller;
|
||||
|
||||
import com.kamco.cd.kamcoback.geojson.service.GeoJsonFileMonitorService;
|
||||
import com.kamco.cd.kamcoback.geojson.service.GeometryConversionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GeoJSON 파일 모니터링 및 처리 API 컨트롤러
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/geojson")
|
||||
@RequiredArgsConstructor
|
||||
public class GeoJsonMonitorController {
|
||||
|
||||
private final GeoJsonFileMonitorService monitorService;
|
||||
private final GeometryConversionService geometryConversionService;
|
||||
|
||||
/**
|
||||
* 모니터링 상태 조회
|
||||
*/
|
||||
@GetMapping("/monitor/status")
|
||||
public Map<String, Object> getMonitorStatus() {
|
||||
return monitorService.getMonitorStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 통계 정보 조회
|
||||
*/
|
||||
@GetMapping("/monitor/stats")
|
||||
public ResponseEntity<Map<String, Object>> getSystemStats() {
|
||||
try {
|
||||
Map<String, Object> stats = monitorService.getSystemStats();
|
||||
return ResponseEntity.ok(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("시스템 통계 조회 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of(
|
||||
"error", "시스템 통계 조회 실패: " + e.getMessage(),
|
||||
"status", "error"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리 초기화 (수동 실행)
|
||||
*/
|
||||
@PostMapping("/monitor/init-directories")
|
||||
public ResponseEntity<Map<String, Object>> initializeDirectories() {
|
||||
try {
|
||||
log.info("디렉토리 초기화 수동 실행 요청");
|
||||
monitorService.initializeDirectoriesManually();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "디렉토리 초기화가 완료되었습니다.",
|
||||
"status", "success"
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("디렉토리 초기화 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of(
|
||||
"error", "디렉토리 초기화 실패: " + e.getMessage(),
|
||||
"status", "error"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동으로 특정 파일 처리
|
||||
*/
|
||||
@PostMapping("/process/file")
|
||||
public ResponseEntity<Map<String, Object>> processFileManually(@RequestParam String filePath) {
|
||||
try {
|
||||
log.info("수동 파일 처리 요청: {}", filePath);
|
||||
monitorService.processFileManually(filePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "파일 처리가 완료되었습니다.",
|
||||
"filePath", filePath,
|
||||
"status", "success"
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("수동 파일 처리 실패: {}", filePath, e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of(
|
||||
"error", "파일 처리 실패: " + e.getMessage(),
|
||||
"filePath", filePath,
|
||||
"status", "error"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미처리된 Geometry 데이터 수동 변환
|
||||
*/
|
||||
@PostMapping("/process/geometry")
|
||||
public ResponseEntity<Map<String, Object>> processUnprocessedGeometry() {
|
||||
try {
|
||||
log.info("미처리 Geometry 변환 수동 실행 요청");
|
||||
List<Long> processedIds = geometryConversionService.processUnprocessedLearnData();
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Geometry 변환이 완료되었습니다.",
|
||||
"processedCount", processedIds.size(),
|
||||
"processedIds", processedIds,
|
||||
"status", "success"
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("Geometry 변환 실패", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of(
|
||||
"error", "Geometry 변환 실패: " + e.getMessage(),
|
||||
"status", "error"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 학습 데이터의 Geometry 변환
|
||||
*/
|
||||
@PostMapping("/process/geometry/convert")
|
||||
public ResponseEntity<Map<String, Object>> convertSpecificGeometry(@RequestBody List<Long> learnDataIds) {
|
||||
try {
|
||||
if (learnDataIds == null || learnDataIds.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "변환할 학습 데이터 ID가 없습니다."));
|
||||
}
|
||||
|
||||
log.info("특정 학습 데이터 Geometry 변환 요청: {}", learnDataIds);
|
||||
List<Long> geometryIds = geometryConversionService.convertToGeometryData(learnDataIds);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Geometry 변환이 완료되었습니다.",
|
||||
"inputCount", learnDataIds.size(),
|
||||
"outputCount", geometryIds.size(),
|
||||
"geometryIds", geometryIds,
|
||||
"status", "success"
|
||||
));
|
||||
} catch (Exception e) {
|
||||
log.error("특정 Geometry 변환 실패: {}", learnDataIds, e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of(
|
||||
"error", "Geometry 변환 실패: " + e.getMessage(),
|
||||
"status", "error"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.kamco.cd.kamcoback.geojson.service;
|
||||
|
||||
import com.kamco.cd.kamcoback.geojson.config.GeoJsonMonitorConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* 압축파일 처리 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ArchiveExtractorService {
|
||||
|
||||
private final GeoJsonMonitorConfig config;
|
||||
|
||||
/**
|
||||
* 압축파일에서 GeoJSON 파일들을 추출
|
||||
*/
|
||||
public Map<String, String> extractGeoJsonFiles(Path archiveFile) throws IOException {
|
||||
Map<String, String> geoJsonContents = new HashMap<>();
|
||||
String fileName = archiveFile.getFileName().toString().toLowerCase();
|
||||
|
||||
log.info("압축파일 추출 시작: {}", archiveFile);
|
||||
|
||||
try {
|
||||
if (fileName.endsWith(".zip")) {
|
||||
extractFromZip(archiveFile, geoJsonContents);
|
||||
} else if (fileName.endsWith(".tar") || fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz")) {
|
||||
extractFromTar(archiveFile, geoJsonContents);
|
||||
} else {
|
||||
throw new IllegalArgumentException("지원하지 않는 압축파일 형식: " + fileName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("압축파일 추출 실패: {}", archiveFile, e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
log.info("압축파일에서 {}개의 GeoJSON 파일을 추출했습니다: {}", geoJsonContents.size(), archiveFile);
|
||||
return geoJsonContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* ZIP 파일에서 GeoJSON 추출
|
||||
*/
|
||||
private void extractFromZip(Path zipFile, Map<String, String> geoJsonContents) throws IOException {
|
||||
try (ZipFile zip = new ZipFile(zipFile.toFile())) {
|
||||
Enumeration<ZipArchiveEntry> entries = zip.getEntries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
|
||||
if (!entry.isDirectory() && isGeoJsonFile(entry.getName())) {
|
||||
try (InputStream inputStream = zip.getInputStream(entry)) {
|
||||
String content = readInputStream(inputStream);
|
||||
geoJsonContents.put(entry.getName(), content);
|
||||
log.debug("ZIP에서 추출: {}", entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TAR 파일에서 GeoJSON 추출
|
||||
*/
|
||||
private void extractFromTar(Path tarFile, Map<String, String> geoJsonContents) throws IOException {
|
||||
String fileName = tarFile.getFileName().toString().toLowerCase();
|
||||
InputStream fileInputStream = Files.newInputStream(tarFile);
|
||||
|
||||
try {
|
||||
// GZIP 압축된 TAR 파일인지 확인
|
||||
if (fileName.endsWith(".gz") || fileName.endsWith(".tgz")) {
|
||||
fileInputStream = new GzipCompressorInputStream(fileInputStream);
|
||||
}
|
||||
|
||||
try (TarArchiveInputStream tarInputStream = new TarArchiveInputStream(fileInputStream)) {
|
||||
ArchiveEntry entry;
|
||||
|
||||
while ((entry = tarInputStream.getNextEntry()) != null) {
|
||||
if (!entry.isDirectory() && isGeoJsonFile(entry.getName())) {
|
||||
String content = readInputStream(tarInputStream);
|
||||
geoJsonContents.put(entry.getName(), content);
|
||||
log.debug("TAR에서 추출: {}", entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
fileInputStream.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("파일 스트림 종료 실패", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* InputStream에서 문자열 읽기
|
||||
*/
|
||||
private String readInputStream(InputStream inputStream) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
|
||||
StringBuilder content = new StringBuilder();
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line).append("\n");
|
||||
}
|
||||
|
||||
return content.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일이 GeoJSON 파일인지 확인
|
||||
*/
|
||||
private boolean isGeoJsonFile(String fileName) {
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
return lowerFileName.endsWith(".geojson") || lowerFileName.endsWith(".json");
|
||||
}
|
||||
|
||||
/**
|
||||
* 지원하는 압축파일인지 확인
|
||||
*/
|
||||
public boolean isSupportedArchive(Path file) {
|
||||
String fileName = file.getFileName().toString().toLowerCase();
|
||||
|
||||
for (String extension : config.getSupportedExtensions()) {
|
||||
if (fileName.endsWith("." + extension)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 크기가 제한 범위 내인지 확인
|
||||
*/
|
||||
public boolean isFileSizeValid(Path file) {
|
||||
try {
|
||||
long fileSize = Files.size(file);
|
||||
boolean isValid = fileSize <= config.getMaxFileSize();
|
||||
|
||||
if (!isValid) {
|
||||
log.warn("파일 크기가 제한을 초과했습니다: {} ({}MB > {}MB)",
|
||||
file, fileSize / 1024 / 1024, config.getMaxFileSize() / 1024 / 1024);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
} catch (IOException e) {
|
||||
log.error("파일 크기 확인 실패: {}", file, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.kamco.cd.kamcoback.geojson.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* GeoJSON 데이터 처리 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GeoJsonDataService {
|
||||
|
||||
private final MapSheetLearnDataRepository mapSheetLearnDataRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* GeoJSON 파일들을 데이터베이스에 저장
|
||||
*/
|
||||
@Transactional
|
||||
public List<Long> processGeoJsonFiles(Map<String, String> geoJsonContents, String archiveFileName) {
|
||||
List<Long> savedIds = new ArrayList<>();
|
||||
|
||||
log.info("GeoJSON 파일 처리 시작: {} ({}개 파일)", archiveFileName, geoJsonContents.size());
|
||||
|
||||
for (Map.Entry<String, String> entry : geoJsonContents.entrySet()) {
|
||||
String fileName = entry.getKey();
|
||||
String geoJsonContent = entry.getValue();
|
||||
|
||||
try {
|
||||
Long savedId = processGeoJsonFile(fileName, geoJsonContent, archiveFileName);
|
||||
if (savedId != null) {
|
||||
savedIds.add(savedId);
|
||||
log.debug("GeoJSON 파일 저장 성공: {} (ID: {})", fileName, savedId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("GeoJSON 파일 처리 실패: {}", fileName, e);
|
||||
// 개별 파일 처리 실패는 전체 처리를 중단시키지 않음
|
||||
}
|
||||
}
|
||||
|
||||
log.info("GeoJSON 파일 처리 완료: {} (성공: {}개, 전체: {}개)",
|
||||
archiveFileName, savedIds.size(), geoJsonContents.size());
|
||||
|
||||
return savedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 GeoJSON 파일을 MapSheetLearnDataEntity로 변환하여 저장
|
||||
*/
|
||||
private Long processGeoJsonFile(String fileName, String geoJsonContent, String archiveFileName) {
|
||||
try {
|
||||
// GeoJSON 파싱 및 검증
|
||||
JsonNode geoJsonNode = objectMapper.readTree(geoJsonContent);
|
||||
validateGeoJsonStructure(geoJsonNode);
|
||||
|
||||
// 파일이 이미 처리되었는지 확인
|
||||
String dataPath = generateDataPath(archiveFileName, fileName);
|
||||
Optional<MapSheetLearnDataEntity> existingData = mapSheetLearnDataRepository.findByDataPath(dataPath);
|
||||
|
||||
if (existingData.isPresent()) {
|
||||
log.warn("이미 처리된 파일입니다: {}", dataPath);
|
||||
return existingData.get().getId();
|
||||
}
|
||||
|
||||
// 새 엔티티 생성 및 저장
|
||||
MapSheetLearnDataEntity entity = createMapSheetLearnDataEntity(fileName, geoJsonContent, archiveFileName, geoJsonNode);
|
||||
MapSheetLearnDataEntity savedEntity = mapSheetLearnDataRepository.save(entity);
|
||||
|
||||
return savedEntity.getId();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("GeoJSON 파일 처리 중 오류 발생: {}", fileName, e);
|
||||
throw new RuntimeException("GeoJSON 파일 처리 실패: " + fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoJSON 구조 검증
|
||||
*/
|
||||
private void validateGeoJsonStructure(JsonNode geoJsonNode) {
|
||||
if (!geoJsonNode.has("type")) {
|
||||
throw new IllegalArgumentException("유효하지 않은 GeoJSON: 'type' 필드가 없습니다.");
|
||||
}
|
||||
|
||||
String type = geoJsonNode.get("type").asText();
|
||||
if (!"FeatureCollection".equals(type) && !"Feature".equals(type) && !"Geometry".equals(type)) {
|
||||
throw new IllegalArgumentException("지원하지 않는 GeoJSON type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MapSheetLearnDataEntity 생성
|
||||
*/
|
||||
private MapSheetLearnDataEntity createMapSheetLearnDataEntity(
|
||||
String fileName, String geoJsonContent, String archiveFileName, JsonNode geoJsonNode) {
|
||||
|
||||
MapSheetLearnDataEntity entity = new MapSheetLearnDataEntity();
|
||||
|
||||
// 기본 정보 설정
|
||||
entity.setDataName(fileName);
|
||||
entity.setDataPath(generateDataPath(archiveFileName, fileName));
|
||||
entity.setDataType("GeoJSON");
|
||||
entity.setDataTitle(extractTitle(fileName, geoJsonNode));
|
||||
|
||||
// CRS 정보 추출 및 설정
|
||||
setCrsInformation(entity, geoJsonNode);
|
||||
|
||||
// JSON 데이터 저장
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> jsonMap = objectMapper.readValue(geoJsonContent, Map.class);
|
||||
entity.setDataJson(jsonMap);
|
||||
} catch (Exception e) {
|
||||
log.warn("JSON 파싱 실패, 원본 텍스트로 저장: {}", fileName, e);
|
||||
// JSON 파싱이 실패하면 원본을 Map 형태로 저장
|
||||
Map<String, Object> fallbackMap = new HashMap<>();
|
||||
fallbackMap.put("raw_content", geoJsonContent);
|
||||
fallbackMap.put("parse_error", e.getMessage());
|
||||
entity.setDataJson(fallbackMap);
|
||||
}
|
||||
|
||||
// 연도 정보 추출 (파일명에서 추출 시도)
|
||||
setYearInformation(entity, fileName);
|
||||
|
||||
// 상태 정보 설정
|
||||
entity.setDataState("PROCESSED");
|
||||
entity.setAnalState("PENDING");
|
||||
|
||||
// 시간 정보 설정
|
||||
Instant now = Instant.now();
|
||||
entity.setCreatedDttm(now);
|
||||
entity.setUpdatedDttm(now);
|
||||
entity.setDataStateDttm(now);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* CRS 정보 설정
|
||||
*/
|
||||
private void setCrsInformation(MapSheetLearnDataEntity entity, JsonNode geoJsonNode) {
|
||||
if (geoJsonNode.has("crs")) {
|
||||
JsonNode crsNode = geoJsonNode.get("crs");
|
||||
if (crsNode.has("type") && crsNode.has("properties")) {
|
||||
String crsType = crsNode.get("type").asText();
|
||||
entity.setDataCrsType(crsType);
|
||||
|
||||
JsonNode propertiesNode = crsNode.get("properties");
|
||||
if (propertiesNode.has("name")) {
|
||||
String crsName = propertiesNode.get("name").asText();
|
||||
entity.setDataCrsTypeName(crsName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// CRS가 명시되지 않은 경우 기본값 설정 (WGS84)
|
||||
entity.setDataCrsType("EPSG");
|
||||
entity.setDataCrsTypeName("EPSG:4326");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연도 정보 추출
|
||||
*/
|
||||
private void setYearInformation(MapSheetLearnDataEntity entity, String fileName) {
|
||||
// 파일명에서 연도 추출 시도 (예: kamco_2021_2022_35813023.geojson)
|
||||
String[] parts = fileName.split("_");
|
||||
for (String part : parts) {
|
||||
if (part.matches("\\d{4}")) { // 4자리 숫자 (연도)
|
||||
try {
|
||||
Integer year = Integer.parseInt(part);
|
||||
if (year >= 1900 && year <= 2100) {
|
||||
if (entity.getDataYyyy() == null) {
|
||||
entity.setDataYyyy(year);
|
||||
} else {
|
||||
entity.setCompareYyyy(year);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// 무시
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 제목 추출
|
||||
*/
|
||||
private String extractTitle(String fileName, JsonNode geoJsonNode) {
|
||||
// GeoJSON 메타데이터에서 제목 추출 시도
|
||||
if (geoJsonNode.has("properties")) {
|
||||
JsonNode properties = geoJsonNode.get("properties");
|
||||
if (properties.has("title")) {
|
||||
return properties.get("title").asText();
|
||||
}
|
||||
if (properties.has("name")) {
|
||||
return properties.get("name").asText();
|
||||
}
|
||||
}
|
||||
|
||||
// 파일명에서 확장자 제거하여 제목으로 사용
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
if (lastDotIndex > 0) {
|
||||
return fileName.substring(0, lastDotIndex);
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 경로 생성
|
||||
*/
|
||||
private String generateDataPath(String archiveFileName, String fileName) {
|
||||
return archiveFileName + "/" + fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 가능한 파일 개수 확인
|
||||
*/
|
||||
public boolean isProcessable(Map<String, String> geoJsonContents) {
|
||||
if (geoJsonContents == null || geoJsonContents.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 최대 처리 가능한 파일 수 제한 (성능 고려)
|
||||
int maxFiles = 50;
|
||||
if (geoJsonContents.size() > maxFiles) {
|
||||
log.warn("처리 가능한 최대 파일 수를 초과했습니다: {} > {}", geoJsonContents.size(), maxFiles);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package com.kamco.cd.kamcoback.geojson.service;
|
||||
|
||||
import com.kamco.cd.kamcoback.geojson.config.GeoJsonMonitorConfig;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataRepository;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataGeomRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* GeoJSON 파일 모니터링 서비스
|
||||
* 지정된 폴더를 주기적으로 모니터링하여 압축파일을 자동으로 처리합니다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GeoJsonFileMonitorService {
|
||||
|
||||
private final GeoJsonMonitorConfig config;
|
||||
private final ArchiveExtractorService archiveExtractorService;
|
||||
private final GeoJsonDataService geoJsonDataService;
|
||||
private final GeometryConversionService geometryConversionService;
|
||||
private final MapSheetLearnDataRepository learnDataRepository;
|
||||
private final MapSheetLearnDataGeomRepository geomRepository;
|
||||
|
||||
/**
|
||||
* 애플리케이션 시작 시 필요한 디렉토리들을 미리 생성
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initializeDirectories() {
|
||||
try {
|
||||
log.info("GeoJSON 모니터링 시스템 초기화 중...");
|
||||
log.info("설정된 경로 - Watch: {}, Processed: {}, Error: {}, Temp: {}",
|
||||
config.getWatchDirectory(), config.getProcessedDirectory(),
|
||||
config.getErrorDirectory(), config.getTempDirectory());
|
||||
|
||||
ensureDirectoriesExist();
|
||||
log.info("GeoJSON 모니터링 시스템 초기화 완료");
|
||||
} catch (Exception e) {
|
||||
log.warn("GeoJSON 모니터링 시스템 초기화 실패 - 스케줄러 실행 시 재시도됩니다", e);
|
||||
// 초기화 실패해도 애플리케이션은 시작되도록 함 (RuntimeException 던지지 않음)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러를 통한 파일 모니터링
|
||||
* 설정된 cron 표현식에 따라 주기적으로 실행
|
||||
*/
|
||||
@Scheduled(cron = "#{@geoJsonMonitorConfig.cronExpression}")
|
||||
public void monitorFiles() {
|
||||
log.debug("파일 모니터링 시작");
|
||||
|
||||
try {
|
||||
// 모니터링 폴더 존재 확인 및 생성
|
||||
ensureDirectoriesExist();
|
||||
|
||||
// 압축파일 검색 및 처리
|
||||
processArchiveFiles();
|
||||
|
||||
// 미처리된 Geometry 변환 작업 수행
|
||||
processUnprocessedGeometryData();
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
log.error("파일 모니터링 중 치명적 오류 발생 - 이번 주기 건너뜀", e);
|
||||
} catch (Exception e) {
|
||||
log.error("파일 모니터링 중 오류 발생", e);
|
||||
}
|
||||
|
||||
log.debug("파일 모니터링 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* 필요한 디렉토리들이 존재하는지 확인하고 생성
|
||||
*/
|
||||
private void ensureDirectoriesExist() {
|
||||
boolean hasError = false;
|
||||
try {
|
||||
createDirectoryIfNotExists(config.getWatchDirectory());
|
||||
} catch (IOException e) {
|
||||
log.error("Watch 디렉토리 생성 실패: {} - {}", config.getWatchDirectory(), e.getMessage());
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
try {
|
||||
createDirectoryIfNotExists(config.getProcessedDirectory());
|
||||
} catch (IOException e) {
|
||||
log.error("Processed 디렉토리 생성 실패: {} - {}", config.getProcessedDirectory(), e.getMessage());
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
try {
|
||||
createDirectoryIfNotExists(config.getErrorDirectory());
|
||||
} catch (IOException e) {
|
||||
log.error("Error 디렉토리 생성 실패: {} - {}", config.getErrorDirectory(), e.getMessage());
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
try {
|
||||
createDirectoryIfNotExists(config.getTempDirectory());
|
||||
} catch (IOException e) {
|
||||
log.error("Temp 디렉토리 생성 실패: {} - {}", config.getTempDirectory(), e.getMessage());
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
log.warn("일부 디렉토리 생성에 실패했습니다. 해당 기능은 제한될 수 있습니다.");
|
||||
log.info("수동으로 다음 디렉토리들을 생성해주세요:");
|
||||
log.info(" - {}", config.getWatchDirectory());
|
||||
log.info(" - {}", config.getProcessedDirectory());
|
||||
log.info(" - {}", config.getErrorDirectory());
|
||||
log.info(" - {}", config.getTempDirectory());
|
||||
} else {
|
||||
log.info("모든 필요한 디렉토리가 준비되었습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리가 존재하지 않으면 생성
|
||||
*/
|
||||
private void createDirectoryIfNotExists(String directory) throws IOException {
|
||||
if (directory == null || directory.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("디렉토리 경로가 비어있습니다.");
|
||||
}
|
||||
|
||||
Path path = Paths.get(directory);
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
try {
|
||||
Files.createDirectories(path);
|
||||
log.info("디렉토리 생성 완료: {}", directory);
|
||||
|
||||
// 디렉토리 권한 설정 (Unix/Linux 환경에서)
|
||||
try {
|
||||
if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
|
||||
// rwxrwxr-x 권한 설정
|
||||
java.nio.file.attribute.PosixFilePermissions.asFileAttribute(
|
||||
java.nio.file.attribute.PosixFilePermissions.fromString("rwxrwxr-x")
|
||||
);
|
||||
}
|
||||
} catch (Exception permissionException) {
|
||||
log.debug("권한 설정 실패 (무시됨): {}", permissionException.getMessage());
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("디렉토리 생성 실패: {} - {}", directory, e.getMessage());
|
||||
throw new IOException("디렉토리를 생성할 수 없습니다: " + directory, e);
|
||||
}
|
||||
} else if (!Files.isDirectory(path)) {
|
||||
throw new IOException("지정된 경로가 디렉토리가 아닙니다: " + directory);
|
||||
} else if (!Files.isWritable(path)) {
|
||||
log.warn("디렉토리에 쓰기 권한이 없습니다: {}", directory);
|
||||
} else {
|
||||
log.debug("디렉토리가 이미 존재합니다: {}", directory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모니터링 폴더에서 압축파일들을 찾아서 처리
|
||||
*/
|
||||
private void processArchiveFiles() {
|
||||
Path watchDir = Paths.get(config.getWatchDirectory());
|
||||
|
||||
// 디렉토리 존재 확인
|
||||
if (!Files.exists(watchDir)) {
|
||||
log.debug("Watch 디렉토리가 존재하지 않습니다: {}", watchDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(watchDir)) {
|
||||
log.warn("Watch 경로가 디렉토리가 아닙니다: {}", watchDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Files.isReadable(watchDir)) {
|
||||
log.warn("Watch 디렉토리에 읽기 권한이 없습니다: {}", watchDir);
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> files = Files.list(watchDir)) {
|
||||
files.filter(Files::isRegularFile)
|
||||
.filter(archiveExtractorService::isSupportedArchive)
|
||||
.filter(archiveExtractorService::isFileSizeValid)
|
||||
.forEach(this::processArchiveFile);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("파일 목록 조회 실패: {}", watchDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 압축파일 처리
|
||||
*/
|
||||
private void processArchiveFile(Path archiveFile) {
|
||||
String fileName = archiveFile.getFileName().toString();
|
||||
log.info("압축파일 처리 시작: {}", fileName);
|
||||
|
||||
try {
|
||||
// 1. 압축파일에서 GeoJSON 파일들 추출
|
||||
Map<String, String> geoJsonContents = archiveExtractorService.extractGeoJsonFiles(archiveFile);
|
||||
|
||||
if (geoJsonContents.isEmpty()) {
|
||||
log.warn("압축파일에서 GeoJSON 파일을 찾을 수 없습니다: {}", fileName);
|
||||
moveFileToError(archiveFile, "GeoJSON 파일 없음");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 처리 가능한 파일 수인지 확인
|
||||
if (!geoJsonDataService.isProcessable(geoJsonContents)) {
|
||||
log.warn("처리할 수 없는 파일입니다: {}", fileName);
|
||||
moveFileToError(archiveFile, "처리 불가능한 파일");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. GeoJSON 데이터를 데이터베이스에 저장
|
||||
List<Long> savedLearnDataIds = geoJsonDataService.processGeoJsonFiles(geoJsonContents, fileName);
|
||||
|
||||
if (savedLearnDataIds.isEmpty()) {
|
||||
log.warn("저장된 학습 데이터가 없습니다: {}", fileName);
|
||||
moveFileToError(archiveFile, "데이터 저장 실패");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Geometry 데이터로 변환
|
||||
List<Long> geometryIds = geometryConversionService.convertToGeometryData(savedLearnDataIds);
|
||||
|
||||
// 5. 처리 완료된 파일을 처리된 폴더로 이동
|
||||
moveFileToProcessed(archiveFile);
|
||||
|
||||
log.info("압축파일 처리 완료: {} (학습 데이터: {}개, Geometry: {}개)",
|
||||
fileName, savedLearnDataIds.size(), geometryIds.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("압축파일 처리 실패: {}", fileName, e);
|
||||
try {
|
||||
moveFileToError(archiveFile, "처리 중 오류 발생: " + e.getMessage());
|
||||
} catch (IOException moveError) {
|
||||
log.error("오류 파일 이동 실패: {}", fileName, moveError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미처리된 Geometry 변환 작업 수행
|
||||
*/
|
||||
private void processUnprocessedGeometryData() {
|
||||
try {
|
||||
List<Long> processedIds = geometryConversionService.processUnprocessedLearnData();
|
||||
if (!processedIds.isEmpty()) {
|
||||
log.info("미처리 Geometry 변환 완료: {}개", processedIds.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("미처리 Geometry 변환 작업 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 처리 완료된 파일을 processed 폴더로 이동
|
||||
*/
|
||||
private void moveFileToProcessed(Path sourceFile) throws IOException {
|
||||
String fileName = sourceFile.getFileName().toString();
|
||||
String timestampedFileName = addTimestamp(fileName);
|
||||
Path targetPath = Paths.get(config.getProcessedDirectory(), timestampedFileName);
|
||||
|
||||
Files.move(sourceFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("파일을 처리된 폴더로 이동: {} -> {}", fileName, timestampedFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 오류가 발생한 파일을 error 폴더로 이동
|
||||
*/
|
||||
private void moveFileToError(Path sourceFile, String errorReason) throws IOException {
|
||||
String fileName = sourceFile.getFileName().toString();
|
||||
String errorFileName = addTimestamp(fileName) + ".error";
|
||||
Path targetPath = Paths.get(config.getErrorDirectory(), errorFileName);
|
||||
|
||||
Files.move(sourceFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
// 오류 정보를 별도 파일로 저장
|
||||
String errorInfoFileName = errorFileName + ".info";
|
||||
Path errorInfoPath = Paths.get(config.getErrorDirectory(), errorInfoFileName);
|
||||
String errorInfo = String.format("파일: %s%n오류 시간: %s%n오류 원인: %s%n",
|
||||
fileName, java.time.Instant.now(), errorReason);
|
||||
Files.write(errorInfoPath, errorInfo.getBytes());
|
||||
|
||||
log.warn("파일을 오류 폴더로 이동: {} (원인: {})", fileName, errorReason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일명에 타임스탬프 추가
|
||||
*/
|
||||
private String addTimestamp(String fileName) {
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
String name = (lastDotIndex > 0) ? fileName.substring(0, lastDotIndex) : fileName;
|
||||
String extension = (lastDotIndex > 0) ? fileName.substring(lastDotIndex) : "";
|
||||
|
||||
return String.format("%s_%d%s", name, System.currentTimeMillis(), extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동으로 특정 파일 처리 (테스트/관리 목적)
|
||||
*/
|
||||
public void processFileManually(String filePath) {
|
||||
Path archiveFile = Paths.get(filePath);
|
||||
|
||||
if (!Files.exists(archiveFile)) {
|
||||
log.error("파일이 존재하지 않습니다: {}", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!archiveExtractorService.isSupportedArchive(archiveFile)) {
|
||||
log.error("지원하지 않는 압축파일 형식입니다: {}", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("수동 파일 처리 시작: {}", filePath);
|
||||
processArchiveFile(archiveFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리 초기화를 수동으로 실행 (API에서 호출 가능)
|
||||
*/
|
||||
public void initializeDirectoriesManually() {
|
||||
log.info("디렉토리 수동 초기화 시작");
|
||||
try {
|
||||
ensureDirectoriesExist();
|
||||
log.info("디렉토리 수동 초기화 완료");
|
||||
} catch (Exception e) {
|
||||
log.error("디렉토리 수동 초기화 실패", e);
|
||||
throw new RuntimeException("디렉토리 초기화 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모니터링 상태 정보 반환
|
||||
*/
|
||||
public Map<String, Object> getMonitorStatus() {
|
||||
return Map.of(
|
||||
"watchDirectory", config.getWatchDirectory(),
|
||||
"processedDirectory", config.getProcessedDirectory(),
|
||||
"errorDirectory", config.getErrorDirectory(),
|
||||
"cronExpression", config.getCronExpression(),
|
||||
"supportedExtensions", config.getSupportedExtensions(),
|
||||
"maxFileSize", config.getMaxFileSize(),
|
||||
"maxFileSizeMB", config.getMaxFileSize() / 1024 / 1024
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 시스템 통계 정보 조회
|
||||
*/
|
||||
public Map<String, Object> getSystemStats() {
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 데이터베이스 통계
|
||||
long totalLearnData = learnDataRepository.count();
|
||||
long totalGeomData = geomRepository.count();
|
||||
long pendingAnalysis = learnDataRepository.countByAnalState("PENDING");
|
||||
|
||||
stats.put("database", Map.of(
|
||||
"totalLearnData", totalLearnData,
|
||||
"totalGeomData", totalGeomData,
|
||||
"pendingAnalysis", pendingAnalysis
|
||||
));
|
||||
|
||||
// 파일 시스템 통계
|
||||
stats.put("fileSystem", getFileSystemStats());
|
||||
|
||||
// 모니터링 설정
|
||||
stats.put("monitoring", Map.of(
|
||||
"isActive", true,
|
||||
"cronExpression", "0/30 * * * * *",
|
||||
"watchDirectory", config.getWatchDirectory(),
|
||||
"processedDirectory", config.getProcessedDirectory(),
|
||||
"errorDirectory", config.getErrorDirectory()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("통계 정보 조회 실패", e);
|
||||
stats.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 시스템 통계 조회
|
||||
*/
|
||||
private Map<String, Object> getFileSystemStats() {
|
||||
Map<String, Object> fileStats = new HashMap<>();
|
||||
|
||||
try {
|
||||
// 각 디렉토리의 파일 수 계산
|
||||
Path watchDir = Paths.get(config.getWatchDirectory());
|
||||
Path processedDir = Paths.get(config.getProcessedDirectory());
|
||||
Path errorDir = Paths.get(config.getErrorDirectory());
|
||||
|
||||
fileStats.put("watchDirectoryCount", countFilesInDirectory(watchDir));
|
||||
fileStats.put("processedDirectoryCount", countFilesInDirectory(processedDir));
|
||||
fileStats.put("errorDirectoryCount", countFilesInDirectory(errorDir));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("파일 시스템 통계 조회 실패: {}", e.getMessage());
|
||||
fileStats.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return fileStats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 디렉토리 내 파일 개수 계산
|
||||
*/
|
||||
private long countFilesInDirectory(Path directory) {
|
||||
if (!Files.exists(directory) || !Files.isDirectory(directory)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try (Stream<Path> files = Files.list(directory)) {
|
||||
return files.filter(Files::isRegularFile).count();
|
||||
} catch (IOException e) {
|
||||
log.warn("디렉토리 파일 계산 실패: {}", directory, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package com.kamco.cd.kamcoback.geojson.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataGeomEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataGeomRepository;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.MapSheetLearnDataRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.locationtech.jts.geom.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Geometry 데이터 변환 서비스
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GeometryConversionService {
|
||||
|
||||
private final MapSheetLearnDataRepository mapSheetLearnDataRepository;
|
||||
private final MapSheetLearnDataGeomRepository mapSheetLearnDataGeomRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final GeometryFactory geometryFactory = new GeometryFactory();
|
||||
|
||||
/**
|
||||
* MapSheetLearnData의 JSON 데이터를 기반으로 Geometry 테이블에 저장
|
||||
*/
|
||||
@Transactional
|
||||
public List<Long> convertToGeometryData(List<Long> learnDataIds) {
|
||||
List<Long> processedIds = new ArrayList<>();
|
||||
|
||||
log.info("Geometry 변환 시작: {} 개의 학습 데이터", learnDataIds.size());
|
||||
|
||||
for (Long dataId : learnDataIds) {
|
||||
try {
|
||||
if (dataId != null) {
|
||||
Optional<MapSheetLearnDataEntity> learnDataOpt = mapSheetLearnDataRepository.findById(dataId);
|
||||
if (learnDataOpt.isPresent()) {
|
||||
List<Long> geometryIds = processLearnDataToGeometry(learnDataOpt.get());
|
||||
processedIds.addAll(geometryIds);
|
||||
log.debug("학습 데이터 {} 에서 {} 개의 geometry 데이터 생성", dataId, geometryIds.size());
|
||||
} else {
|
||||
log.warn("학습 데이터를 찾을 수 없습니다: {}", dataId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Geometry 변환 실패 - 학습 데이터 ID: {}", dataId, e);
|
||||
// 개별 변환 실패는 전체 처리를 중단시키지 않음
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Geometry 변환 완료: {} 개 처리, {} 개의 geometry 생성", learnDataIds.size(), processedIds.size());
|
||||
return processedIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 학습 데이터를 Geometry 데이터로 변환
|
||||
*/
|
||||
private List<Long> processLearnDataToGeometry(MapSheetLearnDataEntity learnData) {
|
||||
List<Long> geometryIds = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 기존 geometry 데이터 삭제 (재생성)
|
||||
mapSheetLearnDataGeomRepository.deleteByDataUid(learnData.getId());
|
||||
|
||||
// JSON 데이터에서 GeoJSON 추출
|
||||
Map<String, Object> dataJson = learnData.getDataJson();
|
||||
if (dataJson == null || dataJson.isEmpty()) {
|
||||
log.warn("JSON 데이터가 없습니다: {}", learnData.getId());
|
||||
return geometryIds;
|
||||
}
|
||||
|
||||
// JSON을 GeoJSON으로 파싱
|
||||
String geoJsonString = objectMapper.writeValueAsString(dataJson);
|
||||
JsonNode geoJsonNode = objectMapper.readTree(geoJsonString);
|
||||
|
||||
// GeoJSON 타입에 따라 처리
|
||||
String type = geoJsonNode.get("type").asText();
|
||||
switch (type) {
|
||||
case "FeatureCollection":
|
||||
geometryIds.addAll(processFeatureCollection(geoJsonNode, learnData));
|
||||
break;
|
||||
case "Feature":
|
||||
Long geometryId = processFeature(geoJsonNode, learnData);
|
||||
if (geometryId != null) {
|
||||
geometryIds.add(geometryId);
|
||||
}
|
||||
break;
|
||||
case "Point":
|
||||
case "LineString":
|
||||
case "Polygon":
|
||||
case "MultiPoint":
|
||||
case "MultiLineString":
|
||||
case "MultiPolygon":
|
||||
Long directGeometryId = processDirectGeometry(geoJsonNode, learnData);
|
||||
if (directGeometryId != null) {
|
||||
geometryIds.add(directGeometryId);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.warn("지원하지 않는 GeoJSON type: {} (데이터 ID: {})", type, learnData.getId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Geometry 변환 실패: 학습 데이터 ID {}", learnData.getId(), e);
|
||||
throw new RuntimeException("Geometry 변환 실패", e);
|
||||
}
|
||||
|
||||
return geometryIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* FeatureCollection 처리
|
||||
*/
|
||||
private List<Long> processFeatureCollection(JsonNode featureCollectionNode, MapSheetLearnDataEntity learnData) {
|
||||
List<Long> geometryIds = new ArrayList<>();
|
||||
|
||||
if (!featureCollectionNode.has("features")) {
|
||||
log.warn("FeatureCollection에 features 배열이 없습니다: {}", learnData.getId());
|
||||
return geometryIds;
|
||||
}
|
||||
|
||||
JsonNode featuresNode = featureCollectionNode.get("features");
|
||||
if (featuresNode.isArray()) {
|
||||
for (JsonNode featureNode : featuresNode) {
|
||||
try {
|
||||
Long geometryId = processFeature(featureNode, learnData);
|
||||
if (geometryId != null) {
|
||||
geometryIds.add(geometryId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Feature 처리 실패 (학습 데이터 ID: {})", learnData.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return geometryIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature 처리
|
||||
*/
|
||||
private Long processFeature(JsonNode featureNode, MapSheetLearnDataEntity learnData) {
|
||||
try {
|
||||
if (!featureNode.has("geometry")) {
|
||||
log.warn("Feature에 geometry가 없습니다: {}", learnData.getId());
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonNode geometryNode = featureNode.get("geometry");
|
||||
JsonNode propertiesNode = featureNode.has("properties") ? featureNode.get("properties") : null;
|
||||
|
||||
return createGeometryEntity(geometryNode, propertiesNode, learnData);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Feature 처리 중 오류 (학습 데이터 ID: {})", learnData.getId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 직접 Geometry 처리
|
||||
*/
|
||||
private Long processDirectGeometry(JsonNode geometryNode, MapSheetLearnDataEntity learnData) {
|
||||
return createGeometryEntity(geometryNode, null, learnData);
|
||||
}
|
||||
|
||||
/**
|
||||
* GeometryEntity 생성 및 저장
|
||||
*/
|
||||
private Long createGeometryEntity(JsonNode geometryNode, JsonNode propertiesNode, MapSheetLearnDataEntity learnData) {
|
||||
try {
|
||||
MapSheetLearnDataGeomEntity geometryEntity = new MapSheetLearnDataGeomEntity();
|
||||
|
||||
// 기본 정보 설정
|
||||
geometryEntity.setDataUid(learnData.getId());
|
||||
geometryEntity.setBeforeYyyy(learnData.getDataYyyy());
|
||||
geometryEntity.setAfterYyyy(learnData.getCompareYyyy());
|
||||
|
||||
// Geometry 변환 및 설정
|
||||
Geometry geometry = parseGeometryFromGeoJson(geometryNode);
|
||||
if (geometry != null) {
|
||||
geometryEntity.setGeom(geometry);
|
||||
geometryEntity.setGeoType(geometry.getGeometryType());
|
||||
|
||||
// 면적 계산 (Polygon인 경우)
|
||||
if (geometry instanceof Polygon || geometry.getGeometryType().contains("Polygon")) {
|
||||
double area = geometry.getArea();
|
||||
geometryEntity.setArea(area);
|
||||
}
|
||||
} else {
|
||||
log.warn("Geometry 변환 실패: {}", geometryNode);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Properties에서 추가 정보 추출
|
||||
if (propertiesNode != null) {
|
||||
extractPropertiesData(geometryEntity, propertiesNode, learnData);
|
||||
}
|
||||
|
||||
// 시간 정보 설정
|
||||
Instant now = Instant.now();
|
||||
geometryEntity.setCreatedDttm(now);
|
||||
geometryEntity.setUpdatedDttm(now);
|
||||
|
||||
// 저장
|
||||
MapSheetLearnDataGeomEntity savedEntity = mapSheetLearnDataGeomRepository.save(geometryEntity);
|
||||
return savedEntity.getId();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("GeometryEntity 생성 실패 (학습 데이터 ID: {})", learnData.getId(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoJSON 노드에서 JTS Geometry 객체 생성
|
||||
*/
|
||||
private Geometry parseGeometryFromGeoJson(JsonNode geometryNode) {
|
||||
try {
|
||||
if (!geometryNode.has("type") || !geometryNode.has("coordinates")) {
|
||||
log.warn("유효하지 않은 Geometry 형식: type 또는 coordinates가 없습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
String geometryType = geometryNode.get("type").asText();
|
||||
JsonNode coordinatesNode = geometryNode.get("coordinates");
|
||||
|
||||
switch (geometryType.toLowerCase()) {
|
||||
case "point":
|
||||
return createPoint(coordinatesNode);
|
||||
case "linestring":
|
||||
return createLineString(coordinatesNode);
|
||||
case "polygon":
|
||||
return createPolygon(coordinatesNode);
|
||||
case "multipoint":
|
||||
return createMultiPoint(coordinatesNode);
|
||||
case "multilinestring":
|
||||
return createMultiLineString(coordinatesNode);
|
||||
case "multipolygon":
|
||||
return createMultiPolygon(coordinatesNode);
|
||||
default:
|
||||
log.warn("지원하지 않는 Geometry 타입: {}", geometryType);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Geometry 파싱 실패", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Point createPoint(JsonNode coordinatesNode) {
|
||||
if (coordinatesNode.size() < 2) return null;
|
||||
double x = coordinatesNode.get(0).asDouble();
|
||||
double y = coordinatesNode.get(1).asDouble();
|
||||
return geometryFactory.createPoint(new Coordinate(x, y));
|
||||
}
|
||||
|
||||
private LineString createLineString(JsonNode coordinatesNode) {
|
||||
List<Coordinate> coords = new ArrayList<>();
|
||||
for (JsonNode coordNode : coordinatesNode) {
|
||||
if (coordNode.size() >= 2) {
|
||||
coords.add(new Coordinate(coordNode.get(0).asDouble(), coordNode.get(1).asDouble()));
|
||||
}
|
||||
}
|
||||
return geometryFactory.createLineString(coords.toArray(new Coordinate[0]));
|
||||
}
|
||||
|
||||
private Polygon createPolygon(JsonNode coordinatesNode) {
|
||||
if (coordinatesNode.size() == 0) return null;
|
||||
|
||||
// Exterior ring
|
||||
JsonNode exteriorRing = coordinatesNode.get(0);
|
||||
List<Coordinate> coords = new ArrayList<>();
|
||||
for (JsonNode coordNode : exteriorRing) {
|
||||
if (coordNode.size() >= 2) {
|
||||
coords.add(new Coordinate(coordNode.get(0).asDouble(), coordNode.get(1).asDouble()));
|
||||
}
|
||||
}
|
||||
|
||||
if (coords.size() < 3) return null;
|
||||
|
||||
// Close ring if not already closed
|
||||
if (!coords.get(0).equals2D(coords.get(coords.size() - 1))) {
|
||||
coords.add(new Coordinate(coords.get(0)));
|
||||
}
|
||||
|
||||
LinearRing shell = geometryFactory.createLinearRing(coords.toArray(new Coordinate[0]));
|
||||
|
||||
// Interior rings (holes)
|
||||
LinearRing[] holes = new LinearRing[coordinatesNode.size() - 1];
|
||||
for (int i = 1; i < coordinatesNode.size(); i++) {
|
||||
JsonNode holeRing = coordinatesNode.get(i);
|
||||
List<Coordinate> holeCoords = new ArrayList<>();
|
||||
for (JsonNode coordNode : holeRing) {
|
||||
if (coordNode.size() >= 2) {
|
||||
holeCoords.add(new Coordinate(coordNode.get(0).asDouble(), coordNode.get(1).asDouble()));
|
||||
}
|
||||
}
|
||||
if (holeCoords.size() >= 3) {
|
||||
if (!holeCoords.get(0).equals2D(holeCoords.get(holeCoords.size() - 1))) {
|
||||
holeCoords.add(new Coordinate(holeCoords.get(0)));
|
||||
}
|
||||
holes[i - 1] = geometryFactory.createLinearRing(holeCoords.toArray(new Coordinate[0]));
|
||||
}
|
||||
}
|
||||
|
||||
return geometryFactory.createPolygon(shell, holes);
|
||||
}
|
||||
|
||||
private MultiPoint createMultiPoint(JsonNode coordinatesNode) {
|
||||
List<Point> points = new ArrayList<>();
|
||||
for (JsonNode pointNode : coordinatesNode) {
|
||||
Point point = createPoint(pointNode);
|
||||
if (point != null) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
return geometryFactory.createMultiPoint(points.toArray(new Point[0]));
|
||||
}
|
||||
|
||||
private MultiLineString createMultiLineString(JsonNode coordinatesNode) {
|
||||
List<LineString> lineStrings = new ArrayList<>();
|
||||
for (JsonNode lineNode : coordinatesNode) {
|
||||
LineString line = createLineString(lineNode);
|
||||
if (line != null) {
|
||||
lineStrings.add(line);
|
||||
}
|
||||
}
|
||||
return geometryFactory.createMultiLineString(lineStrings.toArray(new LineString[0]));
|
||||
}
|
||||
|
||||
private MultiPolygon createMultiPolygon(JsonNode coordinatesNode) {
|
||||
List<Polygon> polygons = new ArrayList<>();
|
||||
for (JsonNode polygonNode : coordinatesNode) {
|
||||
Polygon polygon = createPolygon(polygonNode);
|
||||
if (polygon != null) {
|
||||
polygons.add(polygon);
|
||||
}
|
||||
}
|
||||
return geometryFactory.createMultiPolygon(polygons.toArray(new Polygon[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties에서 추가 정보 추출
|
||||
*/
|
||||
private void extractPropertiesData(MapSheetLearnDataGeomEntity geometryEntity, JsonNode propertiesNode, MapSheetLearnDataEntity learnData) {
|
||||
// CD 정확도 정보
|
||||
if (propertiesNode.has("cd_prob")) {
|
||||
try {
|
||||
double cdProb = propertiesNode.get("cd_prob").asDouble();
|
||||
geometryEntity.setCdProb(cdProb);
|
||||
} catch (Exception e) {
|
||||
log.debug("cd_prob 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Before class 정보
|
||||
if (propertiesNode.has("class_before_name")) {
|
||||
geometryEntity.setClassBeforeName(propertiesNode.get("class_before_name").asText());
|
||||
}
|
||||
if (propertiesNode.has("class_before_prob")) {
|
||||
try {
|
||||
double beforeProb = propertiesNode.get("class_before_prob").asDouble();
|
||||
geometryEntity.setClassBeforeProb(beforeProb);
|
||||
} catch (Exception e) {
|
||||
log.debug("class_before_prob 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
// After class 정보
|
||||
if (propertiesNode.has("class_after_name")) {
|
||||
geometryEntity.setClassAfterName(propertiesNode.get("class_after_name").asText());
|
||||
}
|
||||
if (propertiesNode.has("class_after_prob")) {
|
||||
try {
|
||||
double afterProb = propertiesNode.get("class_after_prob").asDouble();
|
||||
geometryEntity.setClassAfterProb(afterProb);
|
||||
} catch (Exception e) {
|
||||
log.debug("class_after_prob 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 도엽 번호
|
||||
if (propertiesNode.has("map_sheet_num")) {
|
||||
try {
|
||||
long mapSheetNum = propertiesNode.get("map_sheet_num").asLong();
|
||||
geometryEntity.setMapSheetNum(mapSheetNum);
|
||||
} catch (Exception e) {
|
||||
log.debug("map_sheet_num 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 면적 (properties에서 제공되는 경우)
|
||||
if (propertiesNode.has("area")) {
|
||||
try {
|
||||
double area = propertiesNode.get("area").asDouble();
|
||||
geometryEntity.setArea(area);
|
||||
} catch (Exception e) {
|
||||
log.debug("area 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미처리된 학습 데이터들을 찾아서 자동으로 geometry 변환 수행
|
||||
*/
|
||||
@Transactional
|
||||
public List<Long> processUnprocessedLearnData() {
|
||||
// 분석 상태가 PENDING인 학습 데이터 조회
|
||||
List<MapSheetLearnDataEntity> unprocessedData = mapSheetLearnDataRepository.findByAnalState("PENDING");
|
||||
|
||||
if (unprocessedData.isEmpty()) {
|
||||
log.debug("처리할 미완료 학습 데이터가 없습니다.");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("미처리 학습 데이터 {}개에 대해 geometry 변환을 수행합니다.", unprocessedData.size());
|
||||
|
||||
List<Long> processedIds = new ArrayList<>();
|
||||
for (MapSheetLearnDataEntity data : unprocessedData) {
|
||||
try {
|
||||
List<Long> geometryIds = processLearnDataToGeometry(data);
|
||||
processedIds.addAll(geometryIds);
|
||||
|
||||
// 처리 완료 상태로 업데이트
|
||||
data.setAnalState("COMPLETED");
|
||||
mapSheetLearnDataRepository.save(data);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("미처리 학습 데이터 처리 실패: {}", data.getId(), e);
|
||||
// 실패한 경우 ERROR 상태로 설정
|
||||
data.setAnalState("ERROR");
|
||||
mapSheetLearnDataRepository.save(data);
|
||||
}
|
||||
}
|
||||
|
||||
return processedIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.kamco.cd.kamcoback.postgres.repository;
|
||||
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataGeomEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface MapSheetLearnDataGeomRepository extends JpaRepository<MapSheetLearnDataGeomEntity, Long> {
|
||||
|
||||
/**
|
||||
* 데이터 UID로 지오메트리 정보 조회
|
||||
*/
|
||||
List<MapSheetLearnDataGeomEntity> findByDataUid(Long dataUid);
|
||||
|
||||
/**
|
||||
* 도엽 번호로 지오메트리 정보 조회
|
||||
*/
|
||||
List<MapSheetLearnDataGeomEntity> findByMapSheetNum(Long mapSheetNum);
|
||||
|
||||
/**
|
||||
* 연도 범위로 지오메트리 정보 조회
|
||||
*/
|
||||
List<MapSheetLearnDataGeomEntity> findByBeforeYyyyAndAfterYyyy(Integer beforeYyyy, Integer afterYyyy);
|
||||
|
||||
/**
|
||||
* 지오메트리 타입별 조회
|
||||
*/
|
||||
List<MapSheetLearnDataGeomEntity> findByGeoType(String geoType);
|
||||
|
||||
/**
|
||||
* 데이터 UID로 기존 지오메트리 데이터 삭제 (재생성 전에 사용)
|
||||
*/
|
||||
void deleteByDataUid(Long dataUid);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.kamco.cd.kamcoback.postgres.repository;
|
||||
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MapSheetLearnDataEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface MapSheetLearnDataRepository extends JpaRepository<MapSheetLearnDataEntity, Long> {
|
||||
|
||||
/**
|
||||
* 데이터 이름으로 조회
|
||||
*/
|
||||
Optional<MapSheetLearnDataEntity> findByDataName(String dataName);
|
||||
|
||||
/**
|
||||
* 데이터 경로로 조회
|
||||
*/
|
||||
Optional<MapSheetLearnDataEntity> findByDataPath(String dataPath);
|
||||
|
||||
/**
|
||||
* 처리 상태별 조회
|
||||
*/
|
||||
List<MapSheetLearnDataEntity> findByDataState(String dataState);
|
||||
|
||||
/**
|
||||
* 데이터 타입별 조회
|
||||
*/
|
||||
List<MapSheetLearnDataEntity> findByDataType(String dataType);
|
||||
|
||||
/**
|
||||
* 분석 상태별 조회
|
||||
*/
|
||||
List<MapSheetLearnDataEntity> findByAnalState(String analState);
|
||||
|
||||
/**
|
||||
* 분석 상태별 개수 조회
|
||||
*/
|
||||
long countByAnalState(String analState);
|
||||
|
||||
/**
|
||||
* 처리되지 않은 데이터 조회 (data_state가 'PENDING' 또는 null인 것들)
|
||||
*/
|
||||
List<MapSheetLearnDataEntity> findByDataStateIsNullOrDataState(String dataState);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ spring:
|
||||
jpa:
|
||||
show-sql: true
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
ddl-auto: update # 로컬 개발환경에서는 자동 테이블 생성/업데이트
|
||||
properties:
|
||||
hibernate:
|
||||
default_batch_fetch_size: 100 # ✅ 성능 - N+1 쿼리 방지
|
||||
|
||||
@@ -22,7 +22,7 @@ spring:
|
||||
leak-detection-threshold: 60000
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
ddl-auto: update # 테이블이 없으면 생성, 있으면 업데이트
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
@@ -57,3 +57,18 @@ management:
|
||||
include:
|
||||
- "health"
|
||||
|
||||
# GeoJSON 파일 모니터링 설정
|
||||
geojson:
|
||||
monitor:
|
||||
watch-directory: ~/geojson/upload
|
||||
processed-directory: ~/geojson/processed
|
||||
error-directory: ~/geojson/error
|
||||
temp-directory: /tmp/geojson_extract
|
||||
cron-expression: "0/30 * * * * *" # 매 30초마다 실행
|
||||
supported-extensions:
|
||||
- zip
|
||||
- tar
|
||||
- tar.gz
|
||||
- tgz
|
||||
max-file-size: 104857600 # 100MB
|
||||
|
||||
|
||||
32
src/main/resources/db/migration/check_postgis.sql
Normal file
32
src/main/resources/db/migration/check_postgis.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
-- PostGIS extension 및 기본 설정 확인
|
||||
-- 이 스크립트를 PostgreSQL에서 실행하여 PostGIS가 설치되어 있는지 확인
|
||||
|
||||
-- 1. PostGIS extension 설치 (이미 설치되어 있다면 무시됨)
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE EXTENSION IF NOT EXISTS postgis_topology;
|
||||
|
||||
-- 2. 현재 설치된 확장 확인
|
||||
SELECT name, default_version, installed_version
|
||||
FROM pg_available_extensions
|
||||
WHERE name LIKE '%postgis%';
|
||||
|
||||
-- 3. Geometry 타입이 사용 가능한지 확인
|
||||
SELECT typname
|
||||
FROM pg_type
|
||||
WHERE typname = 'geometry';
|
||||
|
||||
-- 4. 테스트용 geometry 컬럼 생성 확인
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 임시 테스트 테이블로 geometry 타입 확인
|
||||
DROP TABLE IF EXISTS temp_geom_test;
|
||||
CREATE TEMP TABLE temp_geom_test (
|
||||
id serial,
|
||||
test_geom geometry(Point, 4326)
|
||||
);
|
||||
RAISE NOTICE 'PostGIS geometry 타입이 정상적으로 작동합니다.';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'PostGIS 설정에 문제가 있습니다: %', SQLERRM;
|
||||
END
|
||||
$$;
|
||||
97
src/main/resources/db/migration/create_geojson_tables.sql
Normal file
97
src/main/resources/db/migration/create_geojson_tables.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- GeoJSON 모니터링 시스템을 위한 필수 테이블 생성 스크립트
|
||||
-- dump-kamco_cds-202511201730.sql에서 추출
|
||||
|
||||
-- 1. 시퀀스 생성
|
||||
CREATE SEQUENCE IF NOT EXISTS public.tb_map_sheet_learn_data_data_uid
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
-- 2. tb_map_sheet_learn_data 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS public.tb_map_sheet_learn_data (
|
||||
data_uid bigint DEFAULT nextval('public.tb_map_sheet_learn_data_data_uid'::regclass) NOT NULL,
|
||||
data_name character varying(128),
|
||||
data_path character varying(255),
|
||||
data_type character varying(128),
|
||||
data_crs_type character varying(128),
|
||||
data_crs_type_name character varying(255),
|
||||
created_dttm timestamp without time zone DEFAULT now(),
|
||||
created_uid bigint,
|
||||
updated_dttm timestamp without time zone DEFAULT now(),
|
||||
updated_uid bigint,
|
||||
compare_yyyy integer,
|
||||
data_yyyy integer,
|
||||
data_json json,
|
||||
data_state character varying(20),
|
||||
data_state_dttm timestamp without time zone DEFAULT now(),
|
||||
data_title character varying(255),
|
||||
anal_map_sheet character varying(255),
|
||||
anal_strt_dttm timestamp without time zone,
|
||||
anal_end_dttm time without time zone,
|
||||
anal_sec bigint,
|
||||
gukuin_used character varying(20),
|
||||
gukuin_used_dttm timestamp without time zone,
|
||||
anal_state character varying(20),
|
||||
CONSTRAINT tb_map_sheet_learn_data_pkey PRIMARY KEY (data_uid)
|
||||
);
|
||||
|
||||
-- 3. 시퀀스 생성 (Geometry용)
|
||||
CREATE SEQUENCE IF NOT EXISTS public.tb_map_sheet_learn_data_geom_geom_uid
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
-- 4. tb_map_sheet_learn_data_geom 테이블 생성
|
||||
CREATE TABLE IF NOT EXISTS public.tb_map_sheet_learn_data_geom (
|
||||
geo_uid bigint DEFAULT nextval('public.tb_map_sheet_learn_data_geom_geom_uid'::regclass) NOT NULL,
|
||||
cd_prob double precision,
|
||||
class_before_name character varying(100),
|
||||
class_before_prob double precision,
|
||||
class_after_name character varying(100),
|
||||
class_after_prob double precision,
|
||||
map_sheet_num bigint,
|
||||
before_yyyy integer,
|
||||
after_yyyy integer,
|
||||
area double precision,
|
||||
geom public.geometry,
|
||||
geo_type character varying(100),
|
||||
data_uid bigint,
|
||||
created_dttm timestamp without time zone,
|
||||
created_uid bigint,
|
||||
updated_dttm timestamp without time zone,
|
||||
updated_uid bigint,
|
||||
CONSTRAINT tb_map_sheet_learn_data_geom_pkey PRIMARY KEY (geo_uid)
|
||||
);
|
||||
|
||||
-- 5. 외래 키 제약 조건
|
||||
ALTER TABLE ONLY public.tb_map_sheet_learn_data_geom
|
||||
ADD CONSTRAINT fk_learn_data_geom_data_uid
|
||||
FOREIGN KEY (data_uid) REFERENCES public.tb_map_sheet_learn_data(data_uid) ON DELETE CASCADE;
|
||||
|
||||
-- 6. 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_data_state ON public.tb_map_sheet_learn_data(data_state);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_anal_state ON public.tb_map_sheet_learn_data(anal_state);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_data_path ON public.tb_map_sheet_learn_data(data_path);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_geom_data_uid ON public.tb_map_sheet_learn_data_geom(data_uid);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_map_sheet_learn_data_geom_geo_type ON public.tb_map_sheet_learn_data_geom(geo_type);
|
||||
|
||||
-- 7. 테이블 코멘트
|
||||
COMMENT ON TABLE public.tb_map_sheet_learn_data IS '학습데이터';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_uid IS '식별키';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_name IS '데이타명';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_path IS '경로';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_type IS '타입';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.data_state IS '처리상태';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data.anal_state IS '분석상태';
|
||||
|
||||
COMMENT ON TABLE public.tb_map_sheet_learn_data_geom IS '학습데이터GEOM정보';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.geo_uid IS '식별키';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.geom IS 'geometry정보';
|
||||
COMMENT ON COLUMN public.tb_map_sheet_learn_data_geom.data_uid IS '데이터식별키';
|
||||
|
||||
-- 완료 메시지
|
||||
SELECT 'GeoJSON 모니터링 시스템 테이블 생성 완료' as message;
|
||||
39058
src/main/resources/db/migration/dump-kamco_cds-202511201730.sql
Normal file
39058
src/main/resources/db/migration/dump-kamco_cds-202511201730.sql
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Reference in New Issue
Block a user