파일생성 추가

This commit is contained in:
2025-12-26 10:23:03 +09:00
parent 8e6b41c9ad
commit baffb147d9
15 changed files with 183 additions and 248 deletions

View File

@@ -17,7 +17,6 @@ public class InferenceResultShpApiController {
private final InferenceResultShpService inferenceResultShpService;
@Operation(summary = "추론결과 데이터 저장", description = "추론결과 데이터 저장")
@PostMapping
public ApiResponseDto<Void> saveInferenceResultData() {

View File

@@ -30,22 +30,21 @@ public class InferenceResultShpDto {
private Geometry geometry;
public Basic(
Long id,
UUID uuid,
Integer stage,
Float cdProb,
Integer input1,
Integer input2,
Long mapId,
String beforeClass,
Float beforeProbability,
String afterClass,
Float afterProbability,
Float area,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
Geometry geometry
) {
Long id,
UUID uuid,
Integer stage,
Float cdProb,
Integer input1,
Integer input2,
Long mapId,
String beforeClass,
Float beforeProbability,
String afterClass,
Float afterProbability,
Float area,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
Geometry geometry) {
this.id = id;
this.uuid = uuid;
this.stage = stage;
@@ -65,22 +64,21 @@ public class InferenceResultShpDto {
public static Basic from(InferenceResultEntity e) {
return new Basic(
e.getId(),
e.getUuid(),
e.getStage(),
e.getCdProb(),
e.getInput1(),
e.getInput2(),
e.getMapId(),
e.getBeforeClass(),
e.getBeforeProbability(),
e.getAfterClass(),
e.getAfterProbability(),
e.getArea(),
e.getCreatedDttm(),
e.getUpdatedDttm(),
e.getGeometry()
);
e.getId(),
e.getUuid(),
e.getStage(),
e.getCdProb(),
e.getInput1(),
e.getInput2(),
e.getMapId(),
e.getBeforeClass(),
e.getBeforeProbability(),
e.getAfterClass(),
e.getAfterProbability(),
e.getArea(),
e.getCreatedDttm(),
e.getUpdatedDttm(),
e.getGeometry());
}
}
}

View File

@@ -48,13 +48,14 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* SHP 파일(.shp/.shx/.dbf/.prj)을 생성한다.
* <p>
* - shpBasePath를 기준으로 파일을 생성한다. 예) /Users/kim/export/shp/1_map_2021_2022 → 1_map_2021_2022.shp → 1_map_2021_2022.shx → 1_map_2021_2022.dbf → 1_map_2021_2022.prj
* <p>
* - geometry 타입은 첫 번째 유효 geometry 기준으로 스키마를 생성한다. - 좌표계는 EPSG:5186으로 설정하며, .prj 파일을 직접 생성한다.
*
* <p>- shpBasePath를 기준으로 파일을 생성한다. 예) /Users/kim/export/shp/1_map_2021_2022 → 1_map_2021_2022.shp
* → 1_map_2021_2022.shx → 1_map_2021_2022.dbf → 1_map_2021_2022.prj
*
* <p>- geometry 타입은 첫 번째 유효 geometry 기준으로 스키마를 생성한다. - 좌표계는 EPSG:5186으로 설정하며, .prj 파일을 직접 생성한다.
*
* @param shpBasePath 확장자를 제외한 SHP 파일 기본 경로
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
*/
@Override
public void writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows) {
@@ -67,13 +68,11 @@ public class GeoToolsShpWriter implements ShpWriter {
// 첫 번째 유효 geometry의 "구체 타입"을 기준으로 스키마를 생성한다.
Geometry firstGeom = firstNonNullGeometry(rows);
if (firstGeom == null) {
throw new IllegalArgumentException(
"SHP 생성 실패: geometry가 전부 null 입니다. path=" + shpBasePath);
throw new IllegalArgumentException("SHP 생성 실패: geometry가 전부 null 입니다. path=" + shpBasePath);
}
@SuppressWarnings("unchecked")
Class<? extends Geometry> geomType =
(Class<? extends Geometry>) firstGeom.getClass();
Class<? extends Geometry> geomType = (Class<? extends Geometry>) firstGeom.getClass();
ShapefileDataStore dataStore = null;
@@ -91,8 +90,7 @@ public class GeoToolsShpWriter implements ShpWriter {
dataStore = createDataStore(shpFile, schema);
// FeatureCollection 생성
DefaultFeatureCollection collection =
buildFeatureCollection(schema, rows);
DefaultFeatureCollection collection = buildFeatureCollection(schema, rows);
// 실제 SHP 파일에 feature 쓰기
writeFeatures(dataStore, collection);
@@ -100,11 +98,7 @@ public class GeoToolsShpWriter implements ShpWriter {
// .prj 파일 직접 생성 (EPSG:5186)
writePrjFile(shpBasePath, crs);
log.info(
"SHP 생성 완료: {} ({} features)",
shpFile.getAbsolutePath(),
collection.size()
);
log.info("SHP 생성 완료: {} ({} features)", shpFile.getAbsolutePath(), collection.size());
} catch (Exception e) {
throw new RuntimeException("SHP 생성 실패: " + shpBasePath, e);
@@ -120,14 +114,16 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* GeoJSON 파일(.geojson)을 생성한다.
* <p>
* - FeatureCollection 형태로 출력한다. - 최상단에 name / crs / properties를 포함한다. - 각 Feature는 polygon 단위로 생성된다. - geometry는 GeoTools GeometryJSON을 사용하여 직렬화한다.
* <p>
* GeoJSON 구조 예: { "type": "FeatureCollection", "name": "stage_input1_input2_mapId", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::5186" } },
* "properties": { ... }, "features": [ ... ] }
*
* <p>- FeatureCollection 형태로 출력한다. - 최상단에 name / crs / properties를 포함한다. - 각 Feature는 polygon 단위로
* 생성된다. - geometry는 GeoTools GeometryJSON을 사용하여 직렬화한다.
*
* <p>GeoJSON 구조 예: { "type": "FeatureCollection", "name": "stage_input1_input2_mapId", "crs": {
* "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::5186" } }, "properties": { ...
* }, "features": [ ... ] }
*
* @param geoJsonPath 생성할 GeoJSON 파일의 전체 경로 (.geojson 포함)
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
*/
@Override
public void writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows) {
@@ -151,13 +147,10 @@ public class GeoToolsShpWriter implements ShpWriter {
root.put("type", "FeatureCollection");
// name: stage_input1_input2_mapId
String name = String.format(
"%d_%d_%d_%d",
first.getStage(),
first.getInput1(),
first.getInput2(),
first.getMapId()
);
String name =
String.format(
"%d_%d_%d_%d",
first.getStage(), first.getInput1(), first.getInput2(), first.getMapId());
root.put("name", name);
// CRS (EPSG:5186)
@@ -175,9 +168,9 @@ public class GeoToolsShpWriter implements ShpWriter {
groupProps.put("input2", first.getInput2());
groupProps.put("map_id", first.getMapId());
// 학습서버 버전은 추후 추가
// groupProps.put("m1", "v1.2222.251223121212");
// groupProps.put("m2", "v2.211.251223121213");
// groupProps.put("m3", "v3.233.251223121214");
// groupProps.put("m1", "v1.2222.251223121212");
// groupProps.put("m2", "v2.211.251223121213");
// groupProps.put("m3", "v3.233.251223121214");
root.set("properties", groupProps);
// features 배열
@@ -193,8 +186,7 @@ public class GeoToolsShpWriter implements ShpWriter {
// feature properties
ObjectNode p = om.createObjectNode();
p.put("polygon_id",
dto.getUuid() != null ? dto.getUuid().toString() : null);
p.put("polygon_id", dto.getUuid() != null ? dto.getUuid().toString() : null);
if (dto.getCdProb() != null) {
p.put("cd_prob", dto.getCdProb());
}
@@ -233,18 +225,11 @@ public class GeoToolsShpWriter implements ShpWriter {
// 파일 쓰기
try (OutputStreamWriter w =
new OutputStreamWriter(
new FileOutputStream(geoJsonFile),
GEOJSON_CHARSET
)) {
new OutputStreamWriter(new FileOutputStream(geoJsonFile), GEOJSON_CHARSET)) {
om.writerWithDefaultPrettyPrinter().writeValue(w, root);
}
log.info(
"GeoJSON 생성 완료: {} ({} features)",
geoJsonFile.getAbsolutePath(),
features.size()
);
log.info("GeoJSON 생성 완료: {} ({} features)", geoJsonFile.getAbsolutePath(), features.size());
} catch (Exception e) {
throw new RuntimeException("GeoJSON 생성 실패: " + geoJsonPath, e);
@@ -253,8 +238,8 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* rows 목록에서 첫 번째로 발견되는 non-null Geometry를 반환한다.
* <p>
* - SHP 스키마 생성 시 geometry 타입 결정을 위해 사용된다.
*
* <p>- SHP 스키마 생성 시 geometry 타입 결정을 위해 사용된다.
*
* @param rows DTO 목록
* @return 첫 번째 non-null Geometry, 없으면 null
@@ -270,17 +255,15 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* SHP 파일에 사용할 SimpleFeatureType(schema)를 생성한다.
* <p>
* - geometry 컬럼은 반드시 첫 번째 컬럼이어야 한다. - DBF 컬럼은 SHP 제약(컬럼명 10자, 길이 제한)을 고려한다.
*
* <p>- geometry 컬럼은 반드시 첫 번째 컬럼이어야 한다. - DBF 컬럼은 SHP 제약(컬럼명 10자, 길이 제한)을 고려한다.
*
* @param geomType geometry의 구체 타입 (Polygon, MultiPolygon 등)
* @param crs 좌표계(EPSG:5186)
* @param crs 좌표계(EPSG:5186)
* @return SimpleFeatureType
*/
private SimpleFeatureType createSchema(
Class<? extends Geometry> geomType,
CoordinateReferenceSystem crs
) {
Class<? extends Geometry> geomType, CoordinateReferenceSystem crs) {
SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
b.setName("inference_result");
b.setCRS(crs);
@@ -305,25 +288,22 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* ShapefileDataStore를 생성하고 스키마를 등록한다.
* <p>
* - DBF 파일 인코딩은 EUC-KR로 설정한다. - spatial index(.qix)를 생성한다.
*
* <p>- DBF 파일 인코딩은 EUC-KR로 설정한다. - spatial index(.qix)를 생성한다.
*
* @param shpFile SHP 파일 객체
* @param schema SimpleFeatureType
* @param schema SimpleFeatureType
* @return 생성된 ShapefileDataStore
*/
private ShapefileDataStore createDataStore(
File shpFile,
SimpleFeatureType schema
) throws Exception {
private ShapefileDataStore createDataStore(File shpFile, SimpleFeatureType schema)
throws Exception {
Map<String, Serializable> params = new HashMap<>();
params.put("url", shpFile.toURI().toURL());
params.put("create spatial index", Boolean.TRUE);
ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
ShapefileDataStore dataStore =
(ShapefileDataStore) factory.createNewDataStore(params);
ShapefileDataStore dataStore = (ShapefileDataStore) factory.createNewDataStore(params);
dataStore.setCharset(DBF_CHARSET);
dataStore.createSchema(schema);
@@ -333,17 +313,15 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* DTO 목록을 SimpleFeatureCollection으로 변환한다.
* <p>
* - DTO 1건당 Feature 1개 생성 - geometry가 null인 데이터는 제외한다.
*
* <p>- DTO 1건당 Feature 1개 생성 - geometry가 null인 데이터는 제외한다.
*
* @param schema FeatureType
* @param rows DTO 목록
* @param rows DTO 목록
* @return DefaultFeatureCollection
*/
private DefaultFeatureCollection buildFeatureCollection(
SimpleFeatureType schema,
List<InferenceResultShpDto.Basic> rows
) {
SimpleFeatureType schema, List<InferenceResultShpDto.Basic> rows) {
DefaultFeatureCollection collection = new DefaultFeatureCollection();
SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema);
@@ -360,13 +338,11 @@ public class GeoToolsShpWriter implements ShpWriter {
builder.add(dto.getMapId());
builder.add(dto.getArea() != null ? dto.getArea().doubleValue() : null);
builder.add(dto.getBeforeClass());
builder.add(dto.getBeforeProbability() != null
? dto.getBeforeProbability().doubleValue()
: null);
builder.add(
dto.getBeforeProbability() != null ? dto.getBeforeProbability().doubleValue() : null);
builder.add(dto.getAfterClass());
builder.add(dto.getAfterProbability() != null
? dto.getAfterProbability().doubleValue()
: null);
builder.add(
dto.getAfterProbability() != null ? dto.getAfterProbability().doubleValue() : null);
SimpleFeature feature = builder.buildFeature(null);
collection.add(feature);
@@ -379,17 +355,14 @@ public class GeoToolsShpWriter implements ShpWriter {
/**
* FeatureCollection을 SHP 파일에 실제로 기록한다.
*
* @param dataStore ShapefileDataStore
* @param dataStore ShapefileDataStore
* @param collection FeatureCollection
*/
private void writeFeatures(
ShapefileDataStore dataStore,
DefaultFeatureCollection collection
) throws Exception {
private void writeFeatures(ShapefileDataStore dataStore, DefaultFeatureCollection collection)
throws Exception {
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureSource featureSource =
dataStore.getFeatureSource(typeName);
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
if (!(featureSource instanceof SimpleFeatureStore store)) {
throw new IllegalStateException("FeatureStore 생성 실패");
@@ -404,21 +377,14 @@ public class GeoToolsShpWriter implements ShpWriter {
* SHP 좌표계 정보를 담은 .prj 파일을 생성한다.
*
* @param shpBasePath SHP 기본 경로 (확장자 제외)
* @param crs 좌표계(EPSG:5186)
* @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");
createDirectories(prjFile);
Files.writeString(
prjFile.toPath(),
crs.toWKT(),
StandardCharsets.UTF_8
);
Files.writeString(prjFile.toPath(), crs.toWKT(), StandardCharsets.UTF_8);
}
/**

View File

@@ -19,14 +19,16 @@ public class InferenceResultShpService {
public void saveInferenceResultData() {
coreService.streamGrouped(1000, (key, entities) -> {
coreService.streamGrouped(
1000,
(key, entities) -> {
// Entity -> DTO
List<InferenceResultShpDto.Basic> dtoList =
entities.stream().map(InferenceResultShpDto.Basic::from).toList();
// Entity -> DTO
List<InferenceResultShpDto.Basic> dtoList =
entities.stream().map(InferenceResultShpDto.Basic::from).toList();
flushGroup(key, dtoList);
});
flushGroup(key, dtoList);
});
}
/**
@@ -41,12 +43,10 @@ public class InferenceResultShpService {
String baseDir = System.getProperty("user.home") + "/export";
// 파일명 stage_input1_input2_mapId
String baseName = String.format(
"%d_%d_%d_%d",
key.stage(), key.mapId(), key.input1(), key.input2()
);
String baseName =
String.format("%d_%d_%d_%d", key.stage(), key.mapId(), key.input1(), key.input2());
String shpBasePath = baseDir + "/shp/" + baseName; // 확장자 없이
String shpBasePath = baseDir + "/shp/" + baseName; // 확장자 없이
String geoJsonPath = baseDir + "/geojson/" + baseName + ".geojson";
// shp: .shp/.shx/.dbf 생성