shp 파일 생성 수정

This commit is contained in:
2025-12-26 16:39:31 +09:00
parent 2a60ddfd36
commit 38aa998aea
8 changed files with 312 additions and 231 deletions

View File

@@ -1,6 +1,7 @@
package com.kamco.cd.kamcoback.inference; package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService; import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -19,15 +20,13 @@ public class InferenceResultShpApiController {
@Operation(summary = "추론결과 데이터 저장", description = "추론결과 데이터 저장") @Operation(summary = "추론결과 데이터 저장", description = "추론결과 데이터 저장")
@PostMapping("/save") @PostMapping("/save")
public ApiResponseDto<String> saveInferenceData() { public ApiResponseDto<InferenceResultShpDto.InferenceCntDto> saveInferenceData() {
inferenceResultShpService.saveInferenceResultData(); return ApiResponseDto.createOK(inferenceResultShpService.saveInferenceResultData());
return ApiResponseDto.createOK("OK");
} }
@Operation(summary = "shp 파일 생성", description = "shp 파일 생성") @Operation(summary = "shp 파일 생성", description = "shp 파일 생성")
@PostMapping("/create") @PostMapping("/create")
public ApiResponseDto<String> createShpFile() { public ApiResponseDto<InferenceResultShpDto.FileCntDto> createShpFile() {
inferenceResultShpService.createShpFile(); return ApiResponseDto.createOK(inferenceResultShpService.createShpFile());
return ApiResponseDto.createOK("OK");
} }
} }

View File

@@ -6,8 +6,8 @@ import java.util.List;
public interface ShpWriter { public interface ShpWriter {
// SHP (.shp/.shx/.dbf) // SHP (.shp/.shx/.dbf)
void writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows); WriteCnt writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows);
// GeoJSON (.geojson) // GeoJSON (.geojson)
void writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows); WriteCnt writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows);
} }

View File

@@ -0,0 +1,17 @@
package com.kamco.cd.kamcoback.inference;
public record WriteCnt(int shp, int shx, int dbf, int prj, int geojson) {
public static WriteCnt zero() {
return new WriteCnt(0, 0, 0, 0, 0);
}
public WriteCnt plus(WriteCnt o) {
return new WriteCnt(
this.shp + o.shp,
this.shx + o.shx,
this.dbf + o.dbf,
this.prj + o.prj,
this.geojson + o.geojson);
}
}

View File

@@ -2,7 +2,9 @@ package com.kamco.cd.kamcoback.inference.dto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import java.util.UUID; import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Geometry;
@@ -61,4 +63,27 @@ public class InferenceResultShpDto {
return d; return d;
} }
} }
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class InferenceCntDto {
int inferenceCnt;
int inferenceGeomCnt;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class FileCntDto {
private int shp;
private int shx;
private int dbf;
private int prj;
private int geojson;
}
} }

View File

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.kamcoback.inference.ShpWriter; import com.kamco.cd.kamcoback.inference.ShpWriter;
import com.kamco.cd.kamcoback.inference.WriteCnt;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -56,12 +57,13 @@ public class GeoToolsShpWriter implements ShpWriter {
* *
* @param shpBasePath 확장자를 제외한 SHP 파일 기본 경로 * @param shpBasePath 확장자를 제외한 SHP 파일 기본 경로
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록 * @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
* @return 이번 호출로 write(생성/덮어쓰기)가 수행된 파일 개수
*/ */
@Override @Override
public void writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows) { public WriteCnt writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows) {
if (rows == null || rows.isEmpty()) { if (rows == null || rows.isEmpty()) {
return; return WriteCnt.zero();
} }
// SHP는 Geometry.class를 허용하지 않으므로 // SHP는 Geometry.class를 허용하지 않으므로
@@ -86,7 +88,7 @@ public class GeoToolsShpWriter implements ShpWriter {
// FeatureType(schema) 생성 // FeatureType(schema) 생성
SimpleFeatureType schema = createSchema(geomType, crs); SimpleFeatureType schema = createSchema(geomType, crs);
// ShapefileDataStore 생성 // ShapefileDataStore 생성 (기존 파일이 있어도 새로 생성/overwrite 동작)
dataStore = createDataStore(shpFile, schema); dataStore = createDataStore(shpFile, schema);
// FeatureCollection 생성 // FeatureCollection 생성
@@ -100,6 +102,9 @@ public class GeoToolsShpWriter implements ShpWriter {
log.info("SHP 생성 완료: {} ({} features)", shpFile.getAbsolutePath(), collection.size()); log.info("SHP 생성 완료: {} ({} features)", shpFile.getAbsolutePath(), collection.size());
// ✅ 덮어쓰기 포함: 이번 호출이 정상 종료되면 4개 파일 write가 발생했다고 카운트
return new WriteCnt(1, 1, 1, 1, 0);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("SHP 생성 실패: " + shpBasePath, e); throw new RuntimeException("SHP 생성 실패: " + shpBasePath, e);
} finally { } finally {
@@ -124,12 +129,13 @@ public class GeoToolsShpWriter implements ShpWriter {
* *
* @param geoJsonPath 생성할 GeoJSON 파일의 전체 경로 (.geojson 포함) * @param geoJsonPath 생성할 GeoJSON 파일의 전체 경로 (.geojson 포함)
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록 * @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
* @return 이번 호출로 write(생성/덮어쓰기)가 수행된 파일 개수
*/ */
@Override @Override
public void writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows) { public WriteCnt writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows) {
if (rows == null || rows.isEmpty()) { if (rows == null || rows.isEmpty()) {
return; return WriteCnt.zero();
} }
try { try {
@@ -167,10 +173,6 @@ public class GeoToolsShpWriter implements ShpWriter {
groupProps.put("input1", first.getInput1()); groupProps.put("input1", first.getInput1());
groupProps.put("input2", first.getInput2()); groupProps.put("input2", first.getInput2());
groupProps.put("map_id", first.getMapId()); groupProps.put("map_id", first.getMapId());
// 학습서버 버전은 추후 추가
// groupProps.put("m1", "v1.2222.251223121212");
// groupProps.put("m2", "v2.211.251223121213");
// groupProps.put("m3", "v3.233.251223121214");
root.set("properties", groupProps); root.set("properties", groupProps);
// features 배열 // features 배열
@@ -231,19 +233,14 @@ public class GeoToolsShpWriter implements ShpWriter {
log.info("GeoJSON 생성 완료: {} ({} features)", geoJsonFile.getAbsolutePath(), features.size()); log.info("GeoJSON 생성 완료: {} ({} features)", geoJsonFile.getAbsolutePath(), features.size());
// 덮어쓰기 포함: 이번 호출이 정상 종료되면 geojson 1개 write로 카운트
return new WriteCnt(0, 0, 0, 0, 1);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("GeoJSON 생성 실패: " + geoJsonPath, e); throw new RuntimeException("GeoJSON 생성 실패: " + geoJsonPath, e);
} }
} }
/**
* rows 목록에서 첫 번째로 발견되는 non-null Geometry를 반환한다.
*
* <p>- SHP 스키마 생성 시 geometry 타입 결정을 위해 사용된다.
*
* @param rows DTO 목록
* @return 첫 번째 non-null Geometry, 없으면 null
*/
private Geometry firstNonNullGeometry(List<InferenceResultShpDto.Basic> rows) { private Geometry firstNonNullGeometry(List<InferenceResultShpDto.Basic> rows) {
for (InferenceResultShpDto.Basic r : rows) { for (InferenceResultShpDto.Basic r : rows) {
if (r != null && r.getGeometry() != null) { if (r != null && r.getGeometry() != null) {
@@ -253,15 +250,6 @@ public class GeoToolsShpWriter implements ShpWriter {
return null; return null;
} }
/**
* SHP 파일에 사용할 SimpleFeatureType(schema)를 생성한다.
*
* <p>- geometry 컬럼은 반드시 첫 번째 컬럼이어야 한다. - DBF 컬럼은 SHP 제약(컬럼명 10자, 길이 제한)을 고려한다.
*
* @param geomType geometry의 구체 타입 (Polygon, MultiPolygon 등)
* @param crs 좌표계(EPSG:5186)
* @return SimpleFeatureType
*/
private SimpleFeatureType createSchema( private SimpleFeatureType createSchema(
Class<? extends Geometry> geomType, CoordinateReferenceSystem crs) { Class<? extends Geometry> geomType, CoordinateReferenceSystem crs) {
SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder(); SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
@@ -286,15 +274,6 @@ public class GeoToolsShpWriter implements ShpWriter {
return b.buildFeatureType(); return b.buildFeatureType();
} }
/**
* ShapefileDataStore를 생성하고 스키마를 등록한다.
*
* <p>- DBF 파일 인코딩은 EUC-KR로 설정한다. - spatial index(.qix)를 생성한다.
*
* @param shpFile SHP 파일 객체
* @param schema SimpleFeatureType
* @return 생성된 ShapefileDataStore
*/
private ShapefileDataStore createDataStore(File shpFile, SimpleFeatureType schema) private ShapefileDataStore createDataStore(File shpFile, SimpleFeatureType schema)
throws Exception { throws Exception {
@@ -311,15 +290,6 @@ public class GeoToolsShpWriter implements ShpWriter {
return dataStore; return dataStore;
} }
/**
* DTO 목록을 SimpleFeatureCollection으로 변환한다.
*
* <p>- DTO 1건당 Feature 1개 생성 - geometry가 null인 데이터는 제외한다.
*
* @param schema FeatureType
* @param rows DTO 목록
* @return DefaultFeatureCollection
*/
private DefaultFeatureCollection buildFeatureCollection( private DefaultFeatureCollection buildFeatureCollection(
SimpleFeatureType schema, List<InferenceResultShpDto.Basic> rows) { SimpleFeatureType schema, List<InferenceResultShpDto.Basic> rows) {
DefaultFeatureCollection collection = new DefaultFeatureCollection(); DefaultFeatureCollection collection = new DefaultFeatureCollection();
@@ -352,12 +322,6 @@ public class GeoToolsShpWriter implements ShpWriter {
return collection; return collection;
} }
/**
* FeatureCollection을 SHP 파일에 실제로 기록한다.
*
* @param dataStore ShapefileDataStore
* @param collection FeatureCollection
*/
private void writeFeatures(ShapefileDataStore dataStore, DefaultFeatureCollection collection) private void writeFeatures(ShapefileDataStore dataStore, DefaultFeatureCollection collection)
throws Exception { throws Exception {
@@ -373,12 +337,6 @@ public class GeoToolsShpWriter implements ShpWriter {
store.getTransaction().commit(); store.getTransaction().commit();
} }
/**
* SHP 좌표계 정보를 담은 .prj 파일을 생성한다.
*
* @param shpBasePath SHP 기본 경로 (확장자 제외)
* @param crs 좌표계(EPSG:5186)
*/
private void writePrjFile(String shpBasePath, CoordinateReferenceSystem crs) throws Exception { private void writePrjFile(String shpBasePath, CoordinateReferenceSystem crs) throws Exception {
File prjFile = new File(shpBasePath + ".prj"); File prjFile = new File(shpBasePath + ".prj");
@@ -387,11 +345,6 @@ public class GeoToolsShpWriter implements ShpWriter {
Files.writeString(prjFile.toPath(), crs.toWKT(), StandardCharsets.UTF_8); Files.writeString(prjFile.toPath(), crs.toWKT(), StandardCharsets.UTF_8);
} }
/**
* 파일이 생성될 디렉토리가 없으면 생성한다.
*
* @param file 생성 대상 파일
*/
private void createDirectories(File file) throws Exception { private void createDirectories(File file) throws Exception {
File parent = file.getParentFile(); File parent = file.getParentFile();
if (parent != null) { if (parent != null) {

View File

@@ -1,9 +1,9 @@
package com.kamco.cd.kamcoback.inference.service; package com.kamco.cd.kamcoback.inference.service;
import com.kamco.cd.kamcoback.inference.ShpWriter; import com.kamco.cd.kamcoback.inference.ShpWriter;
import com.kamco.cd.kamcoback.inference.WriteCnt;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService; import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -17,66 +17,76 @@ public class InferenceResultShpService {
private final InferenceResultShpCoreService coreService; private final InferenceResultShpCoreService coreService;
private final ShpWriter shpWriter; private final ShpWriter shpWriter;
/** inference_results -> tb_map_sheet_anal_data_inference / geom 업서트 */ /** inference_results 테이블을 기준으로 분석 결과 테이블과 도형 테이블을 최신 상태로 반영한다. */
@Transactional @Transactional
public void saveInferenceResultData() { public InferenceResultShpDto.InferenceCntDto saveInferenceResultData() {
coreService.buildInferenceData(); return coreService.buildInferenceData();
} }
/** /**
* dataUid 단위로 재생성(덮어쓰기) - reset(inference false + geom 전부 false) - geom 엔티티 조회 -> dto 변환 - * 분석 데이터 단위로 SHP / GeoJSON 파일을 생성한다.
* shp/geojson 생성 - 성공 geo_uid만 true - inference true *
* <p>처리 흐름: 1. 파일 미생성 상태의 분석 데이터 조회 2. 재생성을 위한 상태 초기화 3. 도형 데이터 조회 4. SHP / GeoJSON 파일 생성 5. 파일
* 생성 완료 상태 반영
*
* <p>중간 실패 시 다음 실행에서 전체 재생성된다.
*/ */
@Transactional @Transactional
public void createShpFile() { public InferenceResultShpDto.FileCntDto createShpFile() {
// TODO 경로는 설정으로 빼는 게 좋음
String baseDir = System.getProperty("user.home") + "/export"; String baseDir = System.getProperty("user.home") + "/export";
int batchSize = 100; // 한번에 처리할 data_uid 개수 int batchSize = 100;
int geomLimit = 500000; // data_uid 당 최대 geom 로딩 수 (메모리/시간 보고 조절) int geomLimit = 500_000;
WriteCnt total = WriteCnt.zero();
List<Long> dataUids = coreService.findPendingDataUids(batchSize); List<Long> dataUids = coreService.findPendingDataUids(batchSize);
for (Long dataUid : dataUids) { for (Long dataUid : dataUids) {
// 1) 덮어쓰기 시작: 리셋 // 재생성을 위한 생성 상태 초기화
coreService.resetForRegenerate(dataUid); coreService.resetForRegenerate(dataUid);
// 2) 생성 대상 조회(엔티티) // 도형 데이터 조회
List<MapSheetAnalDataInferenceGeomEntity> entities = List<InferenceResultShpDto.Basic> dtoList = coreService.loadGeomDtos(dataUid, geomLimit);
coreService.loadGeomEntities(dataUid, geomLimit); if (dtoList.isEmpty()) {
if (entities.isEmpty()) {
// 실패 상태(false 유지) -> 다음 배치에서 다시 덮어쓰기로 시도
continue; continue;
} }
// 3) 엔티티 -> DTO // 파일명 생성 (stage_mapSheet_compare_target)
List<InferenceResultShpDto.Basic> dtoList = InferenceResultShpDto.Basic first = dtoList.get(0);
entities.stream().map(InferenceResultShpDto.Basic::from).toList();
// 4) 파일명: stage_mapSheet_compare_target (첫 row 기준)
MapSheetAnalDataInferenceGeomEntity first = entities.get(0);
String baseName = String baseName =
String.format( String.format(
"%d_%d_%d_%d", "%d_%d_%d_%d",
first.getStage(), first.getStage(), first.getMapId(), first.getInput1(), first.getInput2());
first.getMapSheetNum(),
first.getCompareYyyy(),
first.getTargetYyyy());
String shpBasePath = baseDir + "/shp/" + baseName; // 확장자 없이 String shpBasePath = baseDir + "/shp/" + baseName;
String geoJsonPath = baseDir + "/geojson/" + baseName + ".geojson"; String geoJsonPath = baseDir + "/geojson/" + baseName + ".geojson";
// 5) 파일 생성 (예외 발생 시 성공 마킹 안 됨 -> 다음에 덮어쓰기 재시도) try {
shpWriter.writeShp(shpBasePath, dtoList); // 폴더 안 파일을 세지 않고, Writer가 "이번 호출에서 write한 개수"를 반환
shpWriter.writeGeoJson(geoJsonPath, dtoList); total = total.plus(shpWriter.writeShp(shpBasePath, dtoList));
total = total.plus(shpWriter.writeGeoJson(geoJsonPath, dtoList));
// 6) 성공 마킹: geo_uid만 true // 파일 생성 완료 상태 반영
List<Long> geoUids = List<Long> geoUids = dtoList.stream().map(InferenceResultShpDto.Basic::getGeoUid).toList();
entities.stream().map(MapSheetAnalDataInferenceGeomEntity::getGeoUid).toList();
coreService.markSuccess(dataUid, geoUids); coreService.markSuccess(dataUid, geoUids);
} catch (Exception e) {
// 실패 시 markSuccess 하지 않음 -> 다음 실행에서 재생성
// log.warn("파일 생성 실패: dataUid={}, baseName={}", dataUid, baseName, e);
continue;
} }
} }
InferenceResultShpDto.FileCntDto fileCntDto = new InferenceResultShpDto.FileCntDto();
fileCntDto.setShp(total.shp());
fileCntDto.setShx(total.shx());
fileCntDto.setDbf(total.dbf());
fileCntDto.setPrj(total.prj());
fileCntDto.setGeojson(total.geojson());
return fileCntDto;
}
} }

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.postgres.core; package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import com.kamco.cd.kamcoback.postgres.repository.Inference.InferenceResultRepository; import com.kamco.cd.kamcoback.postgres.repository.Inference.InferenceResultRepository;
import java.util.List; import java.util.List;
@@ -13,22 +14,32 @@ public class InferenceResultShpCoreService {
private final InferenceResultRepository repo; private final InferenceResultRepository repo;
/** inference_results -> (inference, geom) upsert */ /**
* inference_results 기준으로 - tb_map_sheet_anal_data_inference -
* tb_map_sheet_anal_data_inference_geom 테이블을 최신 상태로 구성한다.
*/
@Transactional @Transactional
public void buildInferenceData() { public InferenceResultShpDto.InferenceCntDto buildInferenceData() {
repo.upsertGroupsFromInferenceResults(); int inferenceCnt = repo.upsertGroupsFromInferenceResults();
repo.upsertGeomsFromInferenceResults(); int inferenceGeomCnt = repo.upsertGeomsFromInferenceResults();
InferenceResultShpDto.InferenceCntDto cntDto = new InferenceResultShpDto.InferenceCntDto();
cntDto.setInferenceCnt(inferenceCnt);
cntDto.setInferenceGeomCnt(inferenceGeomCnt);
return cntDto;
} }
/** file_created_yn = false/null 인 data_uid 목록 */ /** 파일 생성이 완료되지 않은 분석 데이터(data_uid) 목록을 조회한다. */
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<Long> findPendingDataUids(int limit) { public List<Long> findPendingDataUids(int limit) {
return repo.findPendingDataUids(limit); return repo.findPendingDataUids(limit);
} }
/** /**
* 재생성 시작: 덮어쓰기 기준(무조건 처음부터) - inference.file_created_yn = false, file_created_dttm = null - * 분석 데이터 재생성을 위해 기존 파일 생성 상태를 초기화한다.
* geom(file_created_yn) 전부 false 리셋 *
* <p>- 분석 데이터(file_created_yn)를 미생성 상태로 변경 - 해당 분석 데이터에 속한 모든 도형의 생성 상태를 미생성으로 변경
*/ */
@Transactional @Transactional
public void resetForRegenerate(Long dataUid) { public void resetForRegenerate(Long dataUid) {
@@ -36,13 +47,25 @@ public class InferenceResultShpCoreService {
repo.resetGeomCreatedByDataUid(dataUid); repo.resetGeomCreatedByDataUid(dataUid);
} }
/** 생성 대상 geom 엔티티 로드 (file_created_yn=false/null + geom not null) */ /**
* 지정된 분석 데이터에 속한 도형 정보를 조회한다.
*
* <p>- 파일 미생성 상태의 도형만 대상 - geometry가 존재하는 도형만 조회
*/
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<MapSheetAnalDataInferenceGeomEntity> loadGeomEntities(Long dataUid, int limit) { public List<InferenceResultShpDto.Basic> loadGeomDtos(Long dataUid, int limit) {
return repo.findGeomEntitiesByDataUid(dataUid, limit); List<MapSheetAnalDataInferenceGeomEntity> entities =
repo.findGeomEntitiesByDataUid(dataUid, limit);
return entities.stream().map(InferenceResultShpDto.Basic::from).toList();
} }
/** 성공 마킹: - 성공 geo_uid만 geom.file_created_yn=true - inference.file_created_yn=true */ /**
* 파일 생성이 성공한 도형 및 분석 데이터에 대해 생성 완료 상태로 갱신한다.
*
* @param dataUid 분석 데이터 UID
* @param geoUids 파일 생성이 완료된 도형 UID 목록
*/
@Transactional @Transactional
public void markSuccess(Long dataUid, List<Long> geoUids) { public void markSuccess(Long dataUid, List<Long> geoUids) {
repo.markGeomCreatedByGeoUids(geoUids); repo.markGeomCreatedByGeoUids(geoUids);

View File

@@ -19,16 +19,26 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
@PersistenceContext private final EntityManager em; @PersistenceContext private final EntityManager em;
private final QMapSheetAnalDataInferenceEntity i = /** tb_map_sheet_anal_data_inference */
private final QMapSheetAnalDataInferenceEntity inferenceEntity =
QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity; QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity;
private final QMapSheetAnalDataInferenceGeomEntity g = /** tb_map_sheet_anal_data_inference_geom */
private final QMapSheetAnalDataInferenceGeomEntity inferenceGeomEntity =
QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity;
// =============================== // ===============================
// Upsert (Native only) // Upsert (Native only)
// =============================== // ===============================
/**
* inference_results 테이블을 기준으로 분석 데이터 단위(stage, compare_yyyy, target_yyyy, map_sheet_num)를
* 생성/갱신한다.
*
* <p>- 최초 생성 시 file_created_yn = false - detecting_cnt는 inference_results 건수 기준
*
* @return 반영된 행 수
*/
@Override @Override
public int upsertGroupsFromInferenceResults() { public int upsertGroupsFromInferenceResults() {
@@ -64,10 +74,17 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
return em.createNativeQuery(sql).executeUpdate(); return em.createNativeQuery(sql).executeUpdate();
} }
/**
* inference_results 테이블을 기준으로 도형 단위(uuid) 분석 결과를 생성/갱신한다.
*
* <p>- uuid 기준 중복 제거(DISTINCT ON) - 최신 updated_dttm 우선 - geometry는 WKB / WKT 모두 처리 - 최초 생성 시
* file_created_yn = false
*
* @return 반영된 행 수
*/
@Override @Override
public int upsertGeomsFromInferenceResults() { public int upsertGeomsFromInferenceResults() {
// class_after_prob 컬럼 매핑 오타 주의(여기 제대로 넣음)
String sql = String sql =
""" """
INSERT INTO tb_map_sheet_anal_data_inference_geom ( INSERT INTO tb_map_sheet_anal_data_inference_geom (
@@ -135,63 +152,92 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
// Jobs // Jobs
// =============================== // ===============================
/**
* 파일 생성이 완료되지 않은 분석 데이터(data_uid) 목록을 조회한다.
*
* @param limit 최대 조회 건수
* @return data_uid 목록
*/
@Override @Override
public List<Long> findPendingDataUids(int limit) { public List<Long> findPendingDataUids(int limit) {
return queryFactory return queryFactory
.select(i.id) // data_uid .select(inferenceEntity.id)
.from(i) .from(inferenceEntity)
.where(i.fileCreatedYn.isFalse().or(i.fileCreatedYn.isNull())) .where(inferenceEntity.fileCreatedYn.isFalse().or(inferenceEntity.fileCreatedYn.isNull()))
.orderBy(i.id.asc()) .orderBy(inferenceEntity.id.asc())
.limit(limit) .limit(limit)
.fetch(); .fetch();
} }
// =============================== // ===============================
// Reset / Mark (전부 ZonedDateTime) // Reset / Mark
// =============================== // ===============================
/**
* 분석 데이터의 파일 생성 상태를 재생성 가능 상태로 초기화한다.
*
* <p>- file_created_yn = false - file_created_dttm = null
*
* @return 갱신된 행 수
*/
@Override @Override
public int resetInferenceCreated(Long dataUid) { public int resetInferenceCreated(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime now = ZonedDateTime.now();
return (int) return (int)
queryFactory queryFactory
.update(i) .update(inferenceEntity)
.set(i.fileCreatedYn, false) .set(inferenceEntity.fileCreatedYn, false)
.set(i.fileCreatedDttm, (ZonedDateTime) null) .set(inferenceEntity.fileCreatedDttm, (ZonedDateTime) null)
.set(i.updatedDttm, now) .set(inferenceEntity.updatedDttm, now)
.where(i.id.eq(dataUid)) .where(inferenceEntity.id.eq(dataUid))
.execute(); .execute();
} }
/**
* 분석 데이터의 파일 생성 완료 상태를 반영한다.
*
* @return 갱신된 행 수
*/
@Override @Override
public int markInferenceCreated(Long dataUid) { public int markInferenceCreated(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime now = ZonedDateTime.now();
return (int) return (int)
queryFactory queryFactory
.update(i) .update(inferenceEntity)
.set(i.fileCreatedYn, true) .set(inferenceEntity.fileCreatedYn, true)
.set(i.fileCreatedDttm, now) .set(inferenceEntity.fileCreatedDttm, now)
.set(i.updatedDttm, now) .set(inferenceEntity.updatedDttm, now)
.where(i.id.eq(dataUid)) .where(inferenceEntity.id.eq(dataUid))
.execute(); .execute();
} }
/**
* 분석 데이터에 속한 모든 도형의 파일 생성 상태를 초기화한다.
*
* @return 갱신된 행 수
*/
@Override @Override
public int resetGeomCreatedByDataUid(Long dataUid) { public int resetGeomCreatedByDataUid(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now(); ZonedDateTime now = ZonedDateTime.now();
return (int) return (int)
queryFactory queryFactory
.update(g) .update(inferenceGeomEntity)
.set(g.fileCreatedYn, false) .set(inferenceGeomEntity.fileCreatedYn, false)
.set(g.fileCreatedDttm, (ZonedDateTime) null) .set(inferenceGeomEntity.fileCreatedDttm, (ZonedDateTime) null)
.set(g.updatedDttm, now) // ✅ 엔티티/Q가 ZonedDateTime이면 정상 .set(inferenceGeomEntity.updatedDttm, now)
.where(g.dataUid.eq(dataUid)) .where(inferenceGeomEntity.dataUid.eq(dataUid))
.execute(); .execute();
} }
/**
* 파일 생성이 완료된 도형(geo_uid)을 생성 완료 상태로 반영한다.
*
* @param geoUids 생성 완료된 도형 UID 목록
* @return 갱신된 행 수
*/
@Override @Override
public int markGeomCreatedByGeoUids(List<Long> geoUids) { public int markGeomCreatedByGeoUids(List<Long> geoUids) {
if (geoUids == null || geoUids.isEmpty()) { if (geoUids == null || geoUids.isEmpty()) {
@@ -202,11 +248,11 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
return (int) return (int)
queryFactory queryFactory
.update(g) .update(inferenceGeomEntity)
.set(g.fileCreatedYn, true) .set(inferenceGeomEntity.fileCreatedYn, true)
.set(g.fileCreatedDttm, now) .set(inferenceGeomEntity.fileCreatedDttm, now)
.set(g.updatedDttm, now) .set(inferenceGeomEntity.updatedDttm, now)
.where(g.geoUid.in(geoUids)) .where(inferenceGeomEntity.geoUid.in(geoUids))
.execute(); .execute();
} }
@@ -214,16 +260,24 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC
// Export source (Entity only) // Export source (Entity only)
// =============================== // ===============================
/**
* SHP / GeoJSON 파일 생성을 위한 도형 데이터 조회
*
* <p>- 특정 분석 데이터(data_uid)에 속한 도형 - geometry 존재 - 파일 미생성 상태만 대상
*/
@Override @Override
public List<MapSheetAnalDataInferenceGeomEntity> findGeomEntitiesByDataUid( public List<MapSheetAnalDataInferenceGeomEntity> findGeomEntitiesByDataUid(
Long dataUid, int limit) { Long dataUid, int limit) {
return queryFactory return queryFactory
.selectFrom(g) .selectFrom(inferenceGeomEntity)
.where( .where(
g.dataUid.eq(dataUid), inferenceGeomEntity.dataUid.eq(dataUid),
g.geom.isNotNull(), inferenceGeomEntity.geom.isNotNull(),
g.fileCreatedYn.isFalse().or(g.fileCreatedYn.isNull())) inferenceGeomEntity
.orderBy(g.geoUid.asc()) .fileCreatedYn
.isFalse()
.or(inferenceGeomEntity.fileCreatedYn.isNull()))
.orderBy(inferenceGeomEntity.geoUid.asc())
.limit(limit) .limit(limit)
.fetch(); .fetch();
} }