Merge pull request '20251216 반영' (#114) from feat/dev_251201 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/114
This commit is contained in:
2025-12-26 15:45:24 +09:00
39 changed files with 1996 additions and 1303 deletions

View File

@@ -38,6 +38,9 @@ dependencies {
//geometry
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation "org.geotools:gt-shapefile:30.0"
implementation "org.geotools:gt-referencing:30.0"
implementation "org.geotools:gt-geojson:30.0"
implementation 'org.locationtech.jts.io:jts-io-common:1.20.0'
implementation 'org.locationtech.jts:jts-core:1.19.0'
implementation 'org.hibernate:hibernate-spatial:6.2.7.Final'

View File

@@ -475,6 +475,20 @@ public class FIleChecker {
return fileTotSize;
}
public static boolean multipartSaveTo(MultipartFile mfile, String targetPath) {
Path tmpSavePath = Paths.get(targetPath);
boolean fileUpload = true;
try {
mfile.transferTo(tmpSavePath);
} catch (IOException e) {
// throw new RuntimeException(e);
return false;
}
return true;
}
public static boolean validationMultipart(MultipartFile mfile) {
// 파일 유효성 검증
if (mfile == null || mfile.isEmpty() || mfile.getSize() == 0) {

View File

@@ -1,22 +0,0 @@
package com.kamco.cd.kamcoback.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** GeoJSON 파일 모니터링 설정 */
@Component
@ConfigurationProperties(prefix = "file.config")
@Getter
@Setter
public class FileConfig {
// private String rootSyncDir = "D:\\app\\original-images\\";
// private String tmpSyncDir = rootSyncDir + "tmp\\";
private String rootSyncDir = "/app/original-images/";
private String tmpSyncDir = rootSyncDir + "tmp/";
private String syncFileExt = "tfw,tif";
}

View File

@@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.config;
import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider;
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
import com.kamco.cd.kamcoback.auth.MenuAuthorizationManager;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
@@ -28,6 +29,7 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationProvider customAuthenticationProvider;
private final MenuAuthorizationManager menuAuthorizationManager;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

View File

@@ -0,0 +1,33 @@
package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.inference.service.InferenceResultShpService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "추론결과 데이터 생성", description = "추론결과 데이터 생성 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/inference/shp")
public class InferenceResultShpApiController {
private final InferenceResultShpService inferenceResultShpService;
@Operation(summary = "추론결과 데이터 저장", description = "추론결과 데이터 저장")
@PostMapping("/save")
public ApiResponseDto<String> saveInferenceData() {
inferenceResultShpService.saveInferenceResultData();
return ApiResponseDto.createOK("OK");
}
@Operation(summary = "shp 파일 생성", description = "shp 파일 생성")
@PostMapping("/create")
public ApiResponseDto<String> createShpFile() {
inferenceResultShpService.createShpFile();
return ApiResponseDto.createOK("OK");
}
}

View File

@@ -0,0 +1,13 @@
package com.kamco.cd.kamcoback.inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import java.util.List;
public interface ShpWriter {
// SHP (.shp/.shx/.dbf)
void writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows);
// GeoJSON (.geojson)
void writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows);
}

View File

@@ -0,0 +1,64 @@
package com.kamco.cd.kamcoback.inference.dto;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.locationtech.jts.geom.Geometry;
public class InferenceResultShpDto {
@Getter
@Setter
public static class Basic {
// ===== 식별 =====
private Long geoUid;
private UUID uuid;
// ===== 그룹 키 =====
private Integer stage;
private Long mapId;
private Integer input1; // compare_yyyy
private Integer input2; // target_yyyy
// ===== 추론 결과 =====
private Float cdProb;
private String beforeClass;
private Float beforeProbability;
private String afterClass;
private Float afterProbability;
// ===== 공간 정보 =====
private Geometry geometry;
private Float area;
/** Entity → DTO 변환 */
public static Basic from(MapSheetAnalDataInferenceGeomEntity e) {
Basic d = new Basic();
d.geoUid = e.getGeoUid();
d.uuid = e.getUuid();
d.stage = e.getStage();
d.mapId = e.getMapSheetNum();
d.input1 = e.getCompareYyyy();
d.input2 = e.getTargetYyyy();
d.cdProb = e.getCdProb();
d.beforeClass = e.getClassBeforeCd();
d.beforeProbability = e.getClassBeforeProb();
d.afterClass = e.getClassAfterCd();
d.afterProbability = e.getClassAfterProb();
d.geometry = e.getGeom();
d.area = e.getArea();
return d;
}
}
}

View File

@@ -0,0 +1,401 @@
package com.kamco.cd.kamcoback.inference.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.kamco.cd.kamcoback.inference.ShpWriter;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.data.SimpleFeatureStore;
import org.geotools.api.data.Transaction;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class GeoToolsShpWriter implements ShpWriter {
// DBF 파일 한글 깨짐 방지를 위해 EUC-KR 사용
private static final Charset DBF_CHARSET = Charset.forName("EUC-KR");
// GeoJSON 출력은 UTF-8
private static final Charset GEOJSON_CHARSET = StandardCharsets.UTF_8;
// 좌표계: Korea 2000 / Central Belt 2010
private static final String EPSG_5186 = "EPSG:5186";
/**
* 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 파일을 직접 생성한다.
*
* @param shpBasePath 확장자를 제외한 SHP 파일 기본 경로
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
*/
@Override
public void writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows) {
if (rows == null || rows.isEmpty()) {
return;
}
// SHP는 Geometry.class를 허용하지 않으므로
// 첫 번째 유효 geometry의 "구체 타입"을 기준으로 스키마를 생성한다.
Geometry firstGeom = firstNonNullGeometry(rows);
if (firstGeom == null) {
throw new IllegalArgumentException("SHP 생성 실패: geometry가 전부 null 입니다. path=" + shpBasePath);
}
@SuppressWarnings("unchecked")
Class<? extends Geometry> geomType = (Class<? extends Geometry>) firstGeom.getClass();
ShapefileDataStore dataStore = null;
try {
File shpFile = new File(shpBasePath + ".shp");
createDirectories(shpFile);
// EPSG:5186 CRS 로딩
CoordinateReferenceSystem crs = CRS.decode(EPSG_5186, false);
// FeatureType(schema) 생성
SimpleFeatureType schema = createSchema(geomType, crs);
// ShapefileDataStore 생성
dataStore = createDataStore(shpFile, schema);
// FeatureCollection 생성
DefaultFeatureCollection collection = buildFeatureCollection(schema, rows);
// 실제 SHP 파일에 feature 쓰기
writeFeatures(dataStore, collection);
// .prj 파일 직접 생성 (EPSG:5186)
writePrjFile(shpBasePath, crs);
log.info("SHP 생성 완료: {} ({} features)", shpFile.getAbsolutePath(), collection.size());
} catch (Exception e) {
throw new RuntimeException("SHP 생성 실패: " + shpBasePath, e);
} finally {
if (dataStore != null) {
try {
dataStore.dispose();
} catch (Exception ignore) {
}
}
}
}
/**
* 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": [ ... ] }
*
* @param geoJsonPath 생성할 GeoJSON 파일의 전체 경로 (.geojson 포함)
* @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
*/
@Override
public void writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows) {
if (rows == null || rows.isEmpty()) {
return;
}
try {
File geoJsonFile = new File(geoJsonPath);
createDirectories(geoJsonFile);
// 그룹 공통 메타 정보는 첫 row 기준
InferenceResultShpDto.Basic first = rows.get(0);
ObjectMapper om = new ObjectMapper();
GeometryJSON gj = new GeometryJSON(15);
// FeatureCollection 루트
ObjectNode root = om.createObjectNode();
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());
root.put("name", name);
// CRS (EPSG:5186)
ObjectNode crs = om.createObjectNode();
crs.put("type", "name");
ObjectNode crsProps = om.createObjectNode();
crsProps.put("name", "urn:ogc:def:crs:EPSG::5186");
crs.set("properties", crsProps);
root.set("crs", crs);
// 그룹 공통 properties
ObjectNode groupProps = om.createObjectNode();
groupProps.put("stage", first.getStage());
groupProps.put("input1", first.getInput1());
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");
root.set("properties", groupProps);
// features 배열
ArrayNode features = om.createArrayNode();
for (InferenceResultShpDto.Basic dto : rows) {
if (dto.getGeometry() == null) {
continue;
}
ObjectNode feature = om.createObjectNode();
feature.put("type", "Feature");
// feature properties
ObjectNode p = om.createObjectNode();
p.put("polygon_id", dto.getUuid() != null ? dto.getUuid().toString() : null);
if (dto.getCdProb() != null) {
p.put("cd_prob", dto.getCdProb());
}
if (dto.getInput1() != null) {
p.put("input1", dto.getInput1());
}
if (dto.getInput2() != null) {
p.put("input2", dto.getInput2());
}
if (dto.getMapId() != null) {
p.put("map_id", dto.getMapId());
}
if (dto.getArea() != null) {
p.put("area", dto.getArea());
}
p.put("before_c", dto.getBeforeClass());
if (dto.getBeforeProbability() != null) {
p.put("before_p", dto.getBeforeProbability());
}
p.put("after_c", dto.getAfterClass());
if (dto.getAfterProbability() != null) {
p.put("after_p", dto.getAfterProbability());
}
feature.set("properties", p);
// geometry
String geomJson = gj.toString(dto.getGeometry());
JsonNode geomNode = om.readTree(geomJson);
feature.set("geometry", geomNode);
features.add(feature);
}
root.set("features", features);
// 파일 쓰기
try (OutputStreamWriter w =
new OutputStreamWriter(new FileOutputStream(geoJsonFile), GEOJSON_CHARSET)) {
om.writerWithDefaultPrettyPrinter().writeValue(w, root);
}
log.info("GeoJSON 생성 완료: {} ({} features)", geoJsonFile.getAbsolutePath(), features.size());
} catch (Exception 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) {
for (InferenceResultShpDto.Basic r : rows) {
if (r != null && r.getGeometry() != null) {
return r.getGeometry();
}
}
return null;
}
/**
* SHP 파일에 사용할 SimpleFeatureType(schema)를 생성한다.
*
* <p>- geometry 컬럼은 반드시 첫 번째 컬럼이어야 한다. - DBF 컬럼은 SHP 제약(컬럼명 10자, 길이 제한)을 고려한다.
*
* @param geomType geometry의 구체 타입 (Polygon, MultiPolygon 등)
* @param crs 좌표계(EPSG:5186)
* @return SimpleFeatureType
*/
private SimpleFeatureType createSchema(
Class<? extends Geometry> geomType, CoordinateReferenceSystem crs) {
SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
b.setName("inference_result");
b.setCRS(crs);
// geometry는 반드시 첫 컬럼
b.add("the_geom", geomType);
// DBF 컬럼 정의 (10자 제한 고려)
b.length(36).add("poly_id", String.class);
b.add("cd_prob", Double.class);
b.add("input1", Integer.class);
b.add("input2", Integer.class);
b.add("map_id", Long.class);
b.add("area", Double.class);
b.length(20).add("before_c", String.class);
b.add("before_p", Double.class);
b.length(20).add("after_c", String.class);
b.add("after_p", Double.class);
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)
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);
dataStore.setCharset(DBF_CHARSET);
dataStore.createSchema(schema);
return dataStore;
}
/**
* DTO 목록을 SimpleFeatureCollection으로 변환한다.
*
* <p>- DTO 1건당 Feature 1개 생성 - geometry가 null인 데이터는 제외한다.
*
* @param schema FeatureType
* @param rows DTO 목록
* @return DefaultFeatureCollection
*/
private DefaultFeatureCollection buildFeatureCollection(
SimpleFeatureType schema, List<InferenceResultShpDto.Basic> rows) {
DefaultFeatureCollection collection = new DefaultFeatureCollection();
SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema);
for (InferenceResultShpDto.Basic dto : rows) {
if (dto == null || dto.getGeometry() == null) {
continue;
}
builder.add(dto.getGeometry());
builder.add(dto.getUuid() != null ? dto.getUuid().toString() : null);
builder.add(dto.getCdProb() != null ? dto.getCdProb().doubleValue() : null);
builder.add(dto.getInput1());
builder.add(dto.getInput2());
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.getAfterClass());
builder.add(
dto.getAfterProbability() != null ? dto.getAfterProbability().doubleValue() : null);
SimpleFeature feature = builder.buildFeature(null);
collection.add(feature);
builder.reset();
}
return collection;
}
/**
* FeatureCollection을 SHP 파일에 실제로 기록한다.
*
* @param dataStore ShapefileDataStore
* @param collection FeatureCollection
*/
private void writeFeatures(ShapefileDataStore dataStore, DefaultFeatureCollection collection)
throws Exception {
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
if (!(featureSource instanceof SimpleFeatureStore store)) {
throw new IllegalStateException("FeatureStore 생성 실패");
}
store.setTransaction(Transaction.AUTO_COMMIT);
store.addFeatures(collection);
store.getTransaction().commit();
}
/**
* SHP 좌표계 정보를 담은 .prj 파일을 생성한다.
*
* @param shpBasePath SHP 기본 경로 (확장자 제외)
* @param crs 좌표계(EPSG:5186)
*/
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);
}
/**
* 파일이 생성될 디렉토리가 없으면 생성한다.
*
* @param file 생성 대상 파일
*/
private void createDirectories(File file) throws Exception {
File parent = file.getParentFile();
if (parent != null) {
Files.createDirectories(parent.toPath());
}
}
}

View File

@@ -0,0 +1,82 @@
package com.kamco.cd.kamcoback.inference.service;
import com.kamco.cd.kamcoback.inference.ShpWriter;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.postgres.core.InferenceResultShpCoreService;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class InferenceResultShpService {
private final InferenceResultShpCoreService coreService;
private final ShpWriter shpWriter;
/** inference_results -> tb_map_sheet_anal_data_inference / geom 업서트 */
@Transactional
public void saveInferenceResultData() {
coreService.buildInferenceData();
}
/**
* dataUid 단위로 재생성(덮어쓰기) - reset(inference false + geom 전부 false) - geom 엔티티 조회 -> dto 변환 -
* shp/geojson 생성 - 성공 geo_uid만 true - inference true
*/
@Transactional
public void createShpFile() {
// TODO 경로는 설정으로 빼는 게 좋음
String baseDir = System.getProperty("user.home") + "/export";
int batchSize = 100; // 한번에 처리할 data_uid 개수
int geomLimit = 500000; // data_uid 당 최대 geom 로딩 수 (메모리/시간 보고 조절)
List<Long> dataUids = coreService.findPendingDataUids(batchSize);
for (Long dataUid : dataUids) {
// 1) 덮어쓰기 시작: 리셋
coreService.resetForRegenerate(dataUid);
// 2) 생성 대상 조회(엔티티)
List<MapSheetAnalDataInferenceGeomEntity> entities =
coreService.loadGeomEntities(dataUid, geomLimit);
if (entities.isEmpty()) {
// 실패 상태(false 유지) -> 다음 배치에서 다시 덮어쓰기로 시도
continue;
}
// 3) 엔티티 -> DTO
List<InferenceResultShpDto.Basic> dtoList =
entities.stream().map(InferenceResultShpDto.Basic::from).toList();
// 4) 파일명: stage_mapSheet_compare_target (첫 row 기준)
MapSheetAnalDataInferenceGeomEntity first = entities.get(0);
String baseName =
String.format(
"%d_%d_%d_%d",
first.getStage(),
first.getMapSheetNum(),
first.getCompareYyyy(),
first.getTargetYyyy());
String shpBasePath = baseDir + "/shp/" + baseName; // 확장자 없이
String geoJsonPath = baseDir + "/geojson/" + baseName + ".geojson";
// 5) 파일 생성 (예외 발생 시 성공 마킹 안 됨 -> 다음에 덮어쓰기 재시도)
shpWriter.writeShp(shpBasePath, dtoList);
shpWriter.writeGeoJson(geoJsonPath, dtoList);
// 6) 성공 마킹: geo_uid만 true
List<Long> geoUids =
entities.stream().map(MapSheetAnalDataInferenceGeomEntity::getGeoUid).toList();
coreService.markSuccess(dataUid, geoUids);
}
}
}

View File

@@ -3,6 +3,10 @@ package com.kamco.cd.kamcoback.mapsheet;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.service.MapSheetMngService;
import io.swagger.v3.oas.annotations.Operation;
@@ -143,6 +147,7 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.findMapSheetErrorList(searchReq));
}
/*
@Operation(summary = "오류데이터 팝업 > 업로드 처리", description = "오류데이터 팝업 > 업로드 처리")
@ApiResponses(
value = {
@@ -163,6 +168,9 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.uploadProcess(hstUidList));
}
*/
/*
@Operation(summary = "오류데이터 팝업 > 추론 제외", description = "오류데이터 팝업 > 추론 제외")
@PutMapping("/except-inference")
public ApiResponseDto<MapSheetMngDto.DmlReturn> updateExceptUseInference(
@@ -170,6 +178,10 @@ public class MapSheetMngApiController {
return ApiResponseDto.ok(mapSheetMngService.updateExceptUseInference(hstUidList));
}
*/
@Operation(summary = "페어 파일 업로드", description = "TFW/TIF 두 파일을 쌍으로 업로드 및 검증")
@PostMapping(value = "/upload-pair", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponseDto<MapSheetMngDto.DmlReturn> uploadPair(
@@ -187,7 +199,7 @@ public class MapSheetMngApiController {
value = {
@ApiResponse(
responseCode = "201",
description = "파일삭제 처리 성공",
description = "파일조회 성공",
content =
@Content(
mediaType = "application/json",
@@ -197,17 +209,19 @@ public class MapSheetMngApiController {
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/mng-file-list")
public ApiResponseDto<List<MapSheetMngDto.MngFilesDto>> findHstUidToMapSheetFileList(
public ApiResponseDto<List<MapSheetMngDto.MngFilesDto>> findByHstUidMapSheetFileList(
@RequestParam @Valid Long hstUid) {
return ApiResponseDto.ok(mapSheetMngService.findHstUidToMapSheetFileList(hstUid));
return ApiResponseDto.ok(mapSheetMngService.findByHstUidMapSheetFileList(hstUid));
}
@Operation(summary = "영상관리 > 파일삭제", description = "영상관리 > 파일삭제")
@Operation(
summary = "영상관리 > 파일사용설정 및 중복제거",
description = "영상관리 >파일사용설정 및 중복제거(중복파일제거 및 선택파일사용설정)")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "파일삭제 처리 성공",
description = "파일사용설정 처리 성공",
content =
@Content(
mediaType = "application/json",
@@ -216,9 +230,47 @@ public class MapSheetMngApiController {
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/del-mng-files")
public ApiResponseDto<MapSheetMngDto.DmlReturn> deleteByFileUidMngFile(
@PutMapping("/update-use-mng-files")
public ApiResponseDto<MapSheetMngDto.DmlReturn> updateUseByFileUidMngFile(
@RequestParam @Valid List<Long> fileUids) {
return ApiResponseDto.ok(mapSheetMngService.deleteByFileUidMngFile(fileUids));
return ApiResponseDto.ok(mapSheetMngService.setUseByFileUidMngFile(fileUids));
}
@Operation(summary = "폴더 조회", description = "폴더 조회 (ROOT:/app/original-images 이하로 경로입력)")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/folder-list")
public ApiResponseDto<FoldersDto> getDir(@RequestBody SrchFoldersDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngService.getFolderAll(srchDto));
}
@Operation(summary = "지정폴더내 파일목록 조회", description = "지정폴더내 파일목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/file-list")
public ApiResponseDto<FilesDto> getFiles(@RequestBody SrchFilesDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngService.getFilesAll(srchDto));
}
}

View File

@@ -1,143 +0,0 @@
package com.kamco.cd.kamcoback.mapsheet;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.code.service.CommonCodeService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.kamcoback.mapsheet.service.MapSheetMngFileCheckerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "영상 관리", description = "영상 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping({"/api/mapsheet"})
public class MapSheetMngFileCheckerApiController {
private final CommonCodeService commonCodeService;
private final MapSheetMngFileCheckerService mapSheetMngFileCheckerService;
@Operation(summary = "폴더 조회", description = "폴더 조회 (ROOT:/app/original-images 이하로 경로입력)")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/folder-list")
public ApiResponseDto<FoldersDto> getDir(@RequestBody SrchFoldersDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.getFolderAll(srchDto));
}
@Operation(summary = "지정폴더내 파일목록 조회", description = "지정폴더내 파일목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/file-list")
public ApiResponseDto<FilesDto> getFiles(@RequestBody SrchFilesDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.getFilesAll(srchDto));
}
/*
@Operation(summary = "파일 업로드", description = "파일 업로드 및 TIF 검증")
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponseDto<String> uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam("targetPath") String targetPath,
@RequestParam(value = "overwrite", required = false, defaultValue = "false")
boolean overwrite,
@RequestParam(value = "hstUid", required = false) Long hstUid) {
return ApiResponseDto.createOK(
mapSheetMngFileCheckerService.uploadFile(file, targetPath, overwrite, hstUid));
}
*/
/*
@Operation(summary = "파일 삭제", description = "중복 파일 등 파일 삭제")
@PostMapping("/delete")
public ApiResponseDto<Boolean> deleteFile(@RequestBody SrchFoldersDto dto) {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.deleteFile(dto.getDirPath()));
}
@Operation(summary = "중복 파일 삭제", description = "중복 데이터 발견 시 기존 데이터를 삭제")
@PostMapping(value = "/delete-file")
public ApiResponseDto<String> deleteDuplicateFile(
@RequestParam("filePath") String filePath, @RequestParam("fileName") String fileName) {
return ApiResponseDto.createOK(
mapSheetMngFileCheckerService.deleteDuplicate(filePath, fileName));
}
*/
/*
@Operation(summary = "지정폴더(하위폴더포함) 파일목록 조회", description = "지정폴더(하위폴더포함) 파일목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/file-all-list")
public ApiResponseDto<FilesDto> getAllFiles(@RequestBody SrchFilesDepthDto srchDto) {
return ApiResponseDto.createOK(mapSheetMngFileCheckerService.getFilesDepthAll(srchDto));
}
@Operation(summary = "영상데이터관리 > 영상파일 동기화", description = "영상파일 동기화")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "동기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "동기화 할수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/sync-process")
public ApiResponseDto<ImageryDto.SyncReturn> syncProcess(
@RequestBody @Valid ImageryDto.searchReq searchReq) {
return ApiResponseDto.ok(mapSheetMngFileCheckerService.syncProcess(searchReq));
}
*/
}

View File

@@ -91,17 +91,6 @@ public class MapSheetMngDto {
@JsonFormatDttm private ZonedDateTime rgstStrtDttm;
@JsonFormatDttm private ZonedDateTime rgstEndDttm;
public String getMngState() {
if (this.syncStateDoneCnt == 0) return "NOTYET";
else if (this.syncStateDoneCnt < this.syncTotCnt) return "PROCESSING";
if ((this.syncNotPaireExecCnt + this.syncDuplicateExecCnt + this.syncFaultExecCnt) > 0)
return "TAKINGERROR";
return "DONE";
}
public String getSyncState() {
if (this.syncStateDoneCnt == 0) return "NOTYET";
@@ -140,8 +129,21 @@ public class MapSheetMngDto {
return this.syncNotPaireExecCnt + this.syncDuplicateExecCnt + this.syncFaultExecCnt;
}
public String getMngState() {
String mngState = "DONE";
if (this.syncStateDoneCnt == 0) mngState = "NOTYET";
else if (this.syncStateDoneCnt < this.syncTotCnt) mngState = "PROCESSING";
if ((this.syncNotPaireExecCnt + this.syncDuplicateExecCnt + this.syncFaultExecCnt) > 0)
mngState = "TAKINGERROR";
return mngState;
}
public String getMngStateName() {
String enumId = this.mngState;
String enumId = this.getMngState();
if (enumId == null || enumId.isEmpty()) {
enumId = "NOTYET";
}

View File

@@ -1,530 +0,0 @@
package com.kamco.cd.kamcoback.mapsheet.service;
import static java.lang.String.CASE_INSENSITIVE_ORDER;
import com.kamco.cd.kamcoback.common.exception.DuplicateFileException;
import com.kamco.cd.kamcoback.common.exception.ValidationException;
import com.kamco.cd.kamcoback.common.utils.FIleChecker;
import com.kamco.cd.kamcoback.config.FileConfig;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDepthDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.ImageryDto;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngFileCheckerCoreService;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngFileEntity;
import com.kamco.cd.kamcoback.postgres.repository.mapsheet.MapSheetMngFileRepository;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetMngFileCheckerService {
private final MapSheetMngFileCheckerCoreService mapSheetMngFileCheckerCoreService;
private final FileConfig fileConfig;
private final MapSheetMngFileRepository mapSheetMngFileRepository;
// @Value("${mapsheet.upload.skipGdalValidation:false}")
// private boolean skipGdalValidation;
public FoldersDto getFolderAll(SrchFoldersDto srchDto) {
Path startPath = Paths.get(fileConfig.getRootSyncDir() + srchDto.getDirPath());
String dirPath = fileConfig.getRootSyncDir() + srchDto.getDirPath();
String sortType = "name desc";
List<FIleChecker.Folder> folderList = FIleChecker.getFolderAll(dirPath);
int folderTotCnt = folderList.size();
int folderErrTotCnt =
(int)
folderList.stream().filter(dto -> dto.getIsValid().toString().equals("false")).count();
return new FoldersDto(dirPath, folderTotCnt, folderErrTotCnt, folderList);
}
public FilesDto getFilesAll(SrchFilesDto srchDto) {
String dirPath = srchDto.getDirPath();
int startPos = srchDto.getStartPos();
int endPos = srchDto.getEndPos();
List<FIleChecker.Basic> files =
FIleChecker.getFilesFromAllDepth(
srchDto.getDirPath(),
"*",
srchDto.getExtension(),
1,
srchDto.getSortType(),
startPos,
endPos);
int fileListPos = 0;
int fileTotCnt = files.size();
long fileTotSize = FIleChecker.getFileTotSize(files);
return new FilesDto(dirPath, fileTotCnt, fileTotSize, files);
}
public FilesDto getFilesDepthAll(SrchFilesDepthDto srchDto) {
String dirPath = srchDto.getDirPath();
int startPos = srchDto.getStartPos();
int endPos = srchDto.getEndPos();
List<FIleChecker.Basic> files =
FIleChecker.getFilesFromAllDepth(
srchDto.getDirPath(),
"*",
srchDto.getExtension(),
srchDto.getMaxDepth(),
srchDto.getSortType(),
startPos,
endPos);
int fileListPos = 0;
int fileTotCnt = files.size();
long fileTotSize = FIleChecker.getFileTotSize(files);
return new FilesDto(dirPath, fileTotCnt, fileTotSize, files);
}
public Set<String> createExtensionSet(String extensionString) {
if (extensionString == null || extensionString.isBlank()) {
return Set.of();
}
// "java, class" -> ["java", " class"] -> [".java", ".class"]
return Arrays.stream(extensionString.split(","))
.map(ext -> ext.trim())
.filter(ext -> !ext.isEmpty())
.map(ext -> "." + ext.toLowerCase())
.collect(Collectors.toSet());
}
public String extractExtension(Path path) {
String filename = path.getFileName().toString();
int lastDotIndex = filename.lastIndexOf('.');
// 확장자가 없거나 파일명이 .으로 끝나는 경우
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return ""; // 빈 문자열 반환
}
// 확장자 추출 및 소문자 변환
return filename.substring(lastDotIndex).toLowerCase();
}
public Comparator<Path> getFileComparator(String sortType) {
// 파일 이름 비교 기본 Comparator (대소문자 무시)
Comparator<Path> nameComparator =
Comparator.comparing(path -> path.getFileName().toString(), CASE_INSENSITIVE_ORDER);
Comparator<Path> dateComparator =
Comparator.comparing(
path -> {
try {
return Files.getLastModifiedTime(path);
} catch (IOException e) {
return FileTime.fromMillis(0);
}
});
if ("name desc".equalsIgnoreCase(sortType)) {
return nameComparator.reversed();
} else if ("date".equalsIgnoreCase(sortType)) {
return dateComparator;
} else if ("date desc".equalsIgnoreCase(sortType)) {
return dateComparator.reversed();
} else {
return nameComparator;
}
}
public ImageryDto.SyncReturn syncProcess(ImageryDto.searchReq searchReq) {
return mapSheetMngFileCheckerCoreService.syncProcess(searchReq);
}
@Transactional
public String uploadFile(MultipartFile file, String targetPath, boolean overwrite, Long hstUid) {
try {
// 파일 유효성 검증
if (file == null || file.isEmpty()) {
throw new ValidationException("업로드 파일이 비어있습니다.");
}
if (file.getOriginalFilename() == null || file.getOriginalFilename().isEmpty()) {
throw new ValidationException("파일명이 유효하지 않습니다.");
}
Path path = Paths.get(targetPath);
// targetPath가 존재하지 않으면 파일 경로로 가정하고 부모 디렉토리 생성
if (!Files.exists(path)) {
// 경로가 확장자로 끝나면 파일로 간주
if (targetPath.matches(".*\\.[a-zA-Z]{3,4}$")) {
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
} else {
// 확장자가 없으면 디렉토리로 간주
Files.createDirectories(path);
path = path.resolve(file.getOriginalFilename());
}
} else if (Files.isDirectory(path)) {
path = path.resolve(file.getOriginalFilename());
}
// 최종 파일의 부모 디렉토리 생성
if (path.getParent() != null && !Files.exists(path.getParent())) {
Files.createDirectories(path.getParent());
}
String filename = path.getFileName().toString();
String ext = FilenameUtils.getExtension(filename).toLowerCase();
String baseName = FilenameUtils.getBaseName(filename);
Path tfwPath =
path.getParent() == null
? Paths.get(baseName + ".tfw")
: path.getParent().resolve(baseName + ".tfw");
Path tifPath =
path.getParent() == null
? Paths.get(baseName + ".tif")
: path.getParent().resolve(baseName + ".tif");
// DB 중복 체크
String parentPathStr = path.getParent() != null ? path.getParent().toString() : "";
boolean dbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(filename, parentPathStr);
// boolean fileExists = Files.exists(path); // 파일 시스템 존재 여부는 체크하지 않음 (DB 기준)
// 이미 존재하는 경우 처리 (DB에만 있는 경우 체크)
if (!overwrite && dbExists) {
throw new DuplicateFileException("동일한 파일이 이미 존재합니다 (DB): " + path.getFileName());
}
// 덮어쓰기인 경우 기존 DB 데이터 삭제 (새로 저장하기 위함)
if (overwrite && dbExists) {
mapSheetMngFileRepository.deleteByFileNameAndFilePath(filename, parentPathStr);
}
// 업로드 파일 저장(덮어쓰기 허용 시 replace)
file.transferTo(path.toFile());
if ("tfw".equals(ext)) {
// TFW 검증
boolean tfwOk = FIleChecker.checkTfw(path.toString());
if (!tfwOk) {
Files.deleteIfExists(path);
throw new ValidationException(
"유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + path.getFileName());
}
// 안내: 같은 베이스의 TIF가 없으면 추후 TIF 업로드 필요
if (!Files.exists(tifPath)) {
// DB 메타 저장은 진행 (향후 쌍 검증 위해)
saveUploadMeta(path, hstUid);
return "TFW 업로드 성공 (매칭되는 TIF가 아직 없습니다).";
}
// TIF가 존재하면 쌍 요건 충족
saveUploadMeta(path, hstUid);
return "TFW 업로드 성공";
}
if ("tif".equals(ext) || "tiff".equals(ext)) {
// GDAL 검증 (항상 수행)
boolean isValidTif = FIleChecker.cmmndGdalInfo(path.toString());
if (!isValidTif) {
Files.deleteIfExists(path);
throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + path.getFileName());
}
// TFW 존재/검증
if (!Files.exists(tfwPath)) {
Files.deleteIfExists(path);
throw new ValidationException("TFW 파일이 존재하지 않습니다: " + tfwPath.getFileName());
}
boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString());
if (!tfwOk) {
Files.deleteIfExists(path);
throw new ValidationException(
"유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwPath.getFileName());
}
saveUploadMeta(path, hstUid);
return "TIF 업로드 성공";
}
// 기타 확장자: 저장만 하고 메타 기록
saveUploadMeta(path, hstUid);
return "업로드 성공";
} catch (ValidationException | DuplicateFileException e) {
// 비즈니스 예외는 그대로 던짐
throw e;
} catch (IOException e) {
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage(), e);
} catch (Exception e) {
throw new IllegalArgumentException("파일 업로드 처리 중 오류 발생: " + e.getMessage(), e);
}
}
@Transactional
public String uploadPair(
MultipartFile tfwFile,
MultipartFile tifFile,
String targetPath,
boolean overwrite,
Long hstUid) {
try {
log.info(
"uploadPair 시작 - targetPath: {}, overwrite: {}, hstUid: {}",
targetPath,
overwrite,
hstUid);
// 파일 유효성 검증
if (tfwFile == null || tfwFile.isEmpty()) {
throw new ValidationException("TFW 파일이 비어있습니다.");
}
if (tifFile == null || tifFile.isEmpty()) {
throw new ValidationException("TIF 파일이 비어있습니다.");
}
if (tfwFile.getOriginalFilename() == null || tfwFile.getOriginalFilename().isEmpty()) {
throw new ValidationException("TFW 파일명이 유효하지 않습니다.");
}
if (tifFile.getOriginalFilename() == null || tifFile.getOriginalFilename().isEmpty()) {
throw new ValidationException("TIF 파일명이 유효하지 않습니다.");
}
log.info(
"파일명 - TFW: {}, TIF: {}", tfwFile.getOriginalFilename(), tifFile.getOriginalFilename());
Path basePath = Paths.get(targetPath);
// targetPath가 존재하지 않으면 디렉토리로 생성
if (!Files.exists(basePath)) {
log.info("대상 경로가 존재하지 않아 디렉토리 생성: {}", basePath);
Files.createDirectories(basePath);
}
// 파일인 경우 부모 디렉토리를 basePath로 사용
if (Files.isRegularFile(basePath)) {
log.info("대상 경로가 파일이므로 부모 디렉토리 사용");
basePath = basePath.getParent();
}
if (Files.isDirectory(basePath)) {
log.info("디렉토리 확인됨: {}", basePath);
// 디렉토리인 경우 파일명 기준으로 경로 생성
Path tfwPath = basePath.resolve(tfwFile.getOriginalFilename());
Path tifPath = basePath.resolve(tifFile.getOriginalFilename());
// 동일 베이스명 확인
String tfwBase = FilenameUtils.getBaseName(tfwPath.getFileName().toString());
String tifBase = FilenameUtils.getBaseName(tifPath.getFileName().toString());
if (!tfwBase.equalsIgnoreCase(tifBase)) {
throw new ValidationException("TFW/TIF 파일명이 동일한 베이스가 아닙니다.");
}
// 디렉토리는 이미 생성되었으므로 추가 생성 불필요
// if (tfwPath.getParent() != null) Files.createDirectories(tfwPath.getParent());
// if (tifPath.getParent() != null) Files.createDirectories(tifPath.getParent());
// DB 중복 체크 및 overwrite 처리 (각 파일별)
String parentPathStr = basePath.toString();
String tfwName = tfwPath.getFileName().toString();
String tifName = tifPath.getFileName().toString();
boolean tfwDbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(tfwName, parentPathStr);
boolean tifDbExists =
mapSheetMngFileRepository.existsByFileNameAndFilePath(tifName, parentPathStr);
if (!overwrite && (tfwDbExists || tifDbExists)) {
throw new DuplicateFileException("동일한 파일이 이미 존재합니다 (DB): " + tfwName + ", " + tifName);
}
if (overwrite) {
if (tfwDbExists)
mapSheetMngFileRepository.deleteByFileNameAndFilePath(tfwName, parentPathStr);
if (tifDbExists)
mapSheetMngFileRepository.deleteByFileNameAndFilePath(tifName, parentPathStr);
}
// 파일 저장
log.info("파일 저장 시작 - TFW: {}, TIF: {}", tfwPath, tifPath);
tfwFile.transferTo(tfwPath.toFile());
tifFile.transferTo(tifPath.toFile());
log.info("파일 저장 완료");
// 검증
log.info("TFW 파일 검증 시작: {}", tfwPath);
boolean tfwOk = FIleChecker.checkTfw(tfwPath.toString());
if (!tfwOk) {
log.warn("TFW 파일 검증 실패: {}", tfwName);
Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TFW 파일입니다 (6줄 숫자 형식 검증 실패): " + tfwName);
}
log.info("TFW 파일 검증 성공");
log.info("TIF 파일 검증 시작: {}", tifPath);
boolean isValidTif = FIleChecker.cmmndGdalInfo(tifPath.toString());
if (!isValidTif) {
log.warn("TIF 파일 검증 실패: {}", tifName);
Files.deleteIfExists(tfwPath);
Files.deleteIfExists(tifPath);
throw new ValidationException("유효하지 않은 TIF 파일입니다 (GDAL 검증 실패): " + tifName);
}
log.info("TIF 파일 검증 성공");
// 메타 저장 (두 파일 각각 저장)
log.info("메타 데이터 저장 시작");
saveUploadMeta(tfwPath, hstUid);
saveUploadMeta(tifPath, hstUid);
log.info("메타 데이터 저장 완료");
return "TFW/TIF 페어 업로드 성공";
} else {
throw new ValidationException("targetPath는 디렉토리여야 합니다.");
}
} catch (ValidationException | DuplicateFileException e) {
// 비즈니스 예외는 그대로 던짐
log.warn("업로드 비즈니스 예외 발생: {}", e.getMessage());
throw e;
} catch (IOException e) {
log.error("파일 I/O 처리 실패: {}", e.getMessage(), e);
throw new IllegalArgumentException("파일 I/O 처리 실패: " + e.getMessage(), e);
} catch (Exception e) {
log.error("파일 업로드 처리 중 예상치 못한 오류 발생: {}", e.getMessage(), e);
throw new IllegalArgumentException("파일 업로드 처리 중 오류 발생: " + e.getMessage(), e);
}
}
private void saveUploadMeta(Path savedPath, Long hstUid) {
String fullPath = savedPath.toAbsolutePath().toString();
String fileName = savedPath.getFileName().toString();
String ext = FilenameUtils.getExtension(fileName);
MapSheetMngFileEntity entity = new MapSheetMngFileEntity();
if (hstUid != null) {
// 히스토리에서 메타 가져오기
var hstOpt = mapSheetMngFileCheckerCoreService.findHstByUid(hstUid);
hstOpt.ifPresent(
hst -> {
entity.setHstUid(hst.getHstUid());
entity.setMngYyyy(hst.getMngYyyy());
entity.setMapSheetNum(hst.getMapSheetNum());
entity.setRefMapSheetNum(hst.getRefMapSheetNum());
});
} else {
// 기존 추정 로직 유지
Integer mngYyyy = extractYearFromPath(fullPath);
String mapSheetNum = extractMapSheetNumFromFileName(fileName);
String refMapSheetNum = null;
if (mapSheetNum != null && !mapSheetNum.isEmpty()) {
try {
long num = Long.parseLong(mapSheetNum);
refMapSheetNum = String.valueOf(num / 1000);
} catch (NumberFormatException ignored) {
}
}
entity.setMngYyyy(mngYyyy);
entity.setMapSheetNum(mapSheetNum);
entity.setRefMapSheetNum(refMapSheetNum);
}
entity.setFilePath(savedPath.getParent() != null ? savedPath.getParent().toString() : "");
entity.setFileName(fileName);
entity.setFileExt(ext);
// 파일 크기 설정
try {
long size = Files.size(savedPath);
entity.setFileSize(size);
} catch (IOException e) {
entity.setFileSize(0L);
}
mapSheetMngFileRepository.save(entity);
}
private Integer extractYearFromPath(String fullPath) {
// 경로에서 4자리 연도를 찾아 가장 근접한 값을 사용
// 예시 경로: /Users/.../original-images/2022/2022_25cm/1/34602
String[] parts = fullPath.split("/");
for (String p : parts) {
if (p.matches("\\d{4}")) {
try {
return Integer.parseInt(p);
} catch (NumberFormatException ignored) {
}
}
}
return null;
}
private String extractMapSheetNumFromFileName(String fileName) {
// 파일명에서 연속된 숫자를 최대한 찾아 사용 (예: 34602027.tif -> 34602027)
String base = FilenameUtils.getBaseName(fileName);
String digits = base.replaceAll("[^0-9]", "");
if (!digits.isEmpty()) {
return digits;
}
return null;
}
@Transactional
public Boolean deleteFile(String filePath) {
try {
Path path = Paths.get(filePath);
return Files.deleteIfExists(path);
} catch (IOException e) {
throw new RuntimeException("파일 삭제 실패: " + e.getMessage());
}
}
@Transactional(readOnly = true)
public List<MapSheetMngFileEntity> findRecentFiles(int limit) {
// 간단히 전체를 불러 정렬/제한 (운영에선 Page 요청으로 변경 권장)
List<MapSheetMngFileEntity> all = new ArrayList<>();
mapSheetMngFileRepository.findAll().forEach(all::add);
all.sort(
(a, b) -> {
// fileUid 기준 내림차순
long av = a.getFileUid() == null ? 0L : a.getFileUid();
long bv = b.getFileUid() == null ? 0L : b.getFileUid();
return Long.compare(bv, av);
});
if (all.size() > limit) {
return all.subList(0, limit);
}
return all;
}
@Transactional
public String deleteDuplicate(String filePath, String fileName) {
try {
Path path = Paths.get(filePath, fileName);
boolean deleted = Files.deleteIfExists(path);
// DB에서도 삭제
mapSheetMngFileRepository.deleteByFileNameAndFilePath(fileName, filePath);
return deleted ? "파일 및 DB 레코드 삭제 완료" : "DB 레코드 삭제 완료 (파일 미존재)";
} catch (IOException e) {
throw new RuntimeException("중복 파일 삭제 실패: " + e.getMessage());
}
}
}

View File

@@ -1,7 +1,10 @@
package com.kamco.cd.kamcoback.mapsheet.service;
import com.kamco.cd.kamcoback.common.utils.FIleChecker;
import com.kamco.cd.kamcoback.config.FileConfig;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.FoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFilesDto;
import com.kamco.cd.kamcoback.mapsheet.dto.FileDto.SrchFoldersDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.AddReq;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.DmlReturn;
@@ -11,7 +14,6 @@ import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngDto;
import com.kamco.cd.kamcoback.mapsheet.dto.MapSheetMngDto.MngFilesDto;
import com.kamco.cd.kamcoback.postgres.core.MapSheetMngCoreService;
import jakarta.validation.Valid;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,7 +33,6 @@ import org.springframework.web.multipart.MultipartFile;
public class MapSheetMngService {
private final MapSheetMngCoreService mapSheetMngCoreService;
private final FileConfig fileConfig;
@Value("${file.sync-root-dir}")
private String syncRootDir;
@@ -71,12 +72,9 @@ public class MapSheetMngService {
}
@Transactional
public DmlReturn mngDataSave(AddReq AddReq) {
return mapSheetMngCoreService.mngDataSave(AddReq);
}
public DmlReturn uploadProcess(@Valid List<Long> hstUidList) {
return mapSheetMngCoreService.uploadProcess(hstUidList);
public DmlReturn mngDataSave(AddReq addReq) {
int execCnt = mapSheetMngCoreService.mngDataSave(addReq);
return new MapSheetMngDto.DmlReturn("success", addReq.getMngYyyy() + "년, " + execCnt + "건 생성");
}
public DmlReturn updateExceptUseInference(@Valid List<Long> hstUidList) {
@@ -112,48 +110,34 @@ public class MapSheetMngService {
// 중복체크
if (!overwrite) {
int tfwCnt =
FIleChecker.getFileCountFromAllDepth(targetYearDir, tfwFile.getOriginalFilename(), "tfw");
int tifCnt =
FIleChecker.getFileCountFromAllDepth(targetYearDir, tfwFile.getOriginalFilename(), "tfw");
if (tfwCnt > 0 || tifCnt > 0) {
String tfwtifMsg = "";
if (tfwCnt > 0) tfwtifMsg = tfwFile.getOriginalFilename();
if (tifCnt > 0) {
if (tfwCnt > 0) tfwtifMsg = "," + tifFile.getOriginalFilename();
else tfwtifMsg = tifFile.getOriginalFilename();
}
return new DmlReturn("duplicate", tfwtifMsg);
}
dmlReturn =
this.duplicateFile(
errDto.getMngYyyy(), tfwFile.getOriginalFilename(), tifFile.getOriginalFilename());
if (dmlReturn.getFlag().equals("duplicate")) return dmlReturn;
}
File directory = new File(tmpPath);
// 멀티파트 파일 tmp폴더 저장(파일형식 체크를 위해)
String tfwTmpPath = tmpPath + tfwFile.getOriginalFilename();
Path tfwTmpSavePath = Paths.get(tfwTmpPath);
String tifTmpPath = tmpPath + tifFile.getOriginalFilename();
Path tifTmpSavePath = Paths.get(tifTmpPath);
boolean fileUpload = true;
try {
tfwFile.transferTo(tfwTmpSavePath);
tifFile.transferTo(tifTmpSavePath);
} catch (IOException e) {
// throw new RuntimeException(e);
if (!FIleChecker.multipartSaveTo(tfwFile, tfwTmpPath))
return new DmlReturn("fail", "UPLOAD ERROR");
if (!FIleChecker.multipartSaveTo(tifFile, tifTmpPath))
return new DmlReturn("fail", "UPLOAD ERROR");
}
if (!FIleChecker.cmmndGdalInfo(tifTmpPath)) return new DmlReturn("fail", "TIF TYPE ERROR");
if (!FIleChecker.checkTfw(tfwTmpPath)) return new DmlReturn("fail", "TFW TYPE ERROR");
// 싱크파일목록으로 업로드 경로 확인
List<MngFilesDto> mngFiles = mapSheetMngCoreService.findIdToMapSheetFileList(hstUid);
List<MngFilesDto> mngFiles = mapSheetMngCoreService.findByHstUidMapSheetFileList(hstUid);
String uploadPath = "";
for (MngFilesDto dto : mngFiles) {
uploadPath = dto.getFilePath();
break;
}
Path tfwTmpSavePath = Paths.get(tfwTmpPath);
Path tifTmpSavePath = Paths.get(tifTmpPath);
Path tfwTargetPath = null;
Path tifTargetPath = null;
@@ -212,6 +196,41 @@ public class MapSheetMngService {
return new DmlReturn("success", "파일 업로드 완료되었습니다.");
}
public List<MngFilesDto> findByHstUidMapSheetFileList(Long hstUid) {
return mapSheetMngCoreService.findByHstUidMapSheetFileList(hstUid);
}
@Transactional
public DmlReturn setUseByFileUidMngFile(List<Long> fileUids) {
long hstUid = 0;
DmlReturn dmlReturn = new DmlReturn("success", "정상처리되었습니다.");
MapSheetMngDto.SyncCheckStateReqUpdateDto reqDto =
new MapSheetMngDto.SyncCheckStateReqUpdateDto();
for (Long uid : fileUids) {
MapSheetMngDto.MngFilesDto dto = mapSheetMngCoreService.findByFileUidMapSheetFile(uid);
reqDto.setHstUid(dto.getHstUid());
reqDto.setFilePath(dto.getFilePath());
reqDto.setSyncCheckState("DONE");
if (dto.getFileExt().equals("tif")) reqDto.setSyncCheckTifFileName(dto.getFileName());
else if (dto.getFileExt().equals("tfw")) reqDto.setSyncCheckTfwFileName(dto.getFileName());
mapSheetMngCoreService.updateByFileUidFileState(uid, "DONE");
}
// 선택제외 삭제처리
mapSheetMngCoreService.deleteByNotInFileUidMngFile(reqDto.getHstUid(), fileUids);
// Hst(내역) 테이블 상태 업데이트
mapSheetMngCoreService.updateByHstUidSyncCheckState(reqDto);
return new DmlReturn("success", fileUids.size() + "개 파일이 사용설정되었습니다.");
}
public DmlReturn validationFile(MultipartFile tfwFile, MultipartFile tifFile) {
if (!FIleChecker.validationMultipart(tfwFile)) return new DmlReturn("fail", "TFW SIZE 오류");
else if (!FIleChecker.validationMultipart(tifFile)) return new DmlReturn("fail", "TFW SIZE 오류");
@@ -223,38 +242,60 @@ public class MapSheetMngService {
return new DmlReturn("success", "파일체크");
}
public List<MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) {
return mapSheetMngCoreService.findHstUidToMapSheetFileList(hstUid);
public DmlReturn duplicateFile(int mngYyyy, String tfwFileName, String tifFileName) {
int tfwCnt = mapSheetMngCoreService.findByYearFileNameFileCount(mngYyyy, tfwFileName);
int tifCnt = mapSheetMngCoreService.findByYearFileNameFileCount(mngYyyy, tifFileName);
if (tfwCnt > 0 || tifCnt > 0) {
String resMsg = "";
if (tfwCnt > 0) resMsg = tfwFileName;
if (tifCnt > 0) {
if (tfwCnt > 0) resMsg = resMsg + "," + tifFileName;
else resMsg = tifFileName;
}
return new DmlReturn("duplicate", resMsg);
}
@Transactional
public DmlReturn deleteByFileUidMngFile(List<Long> fileUids) {
long hstUid = 0;
// hstUid = 149049;
for (Long uid : fileUids) {
MapSheetMngDto.MngFilesDto dto = mapSheetMngCoreService.findIdToMapSheetFile(uid);
hstUid = dto.getHstUid();
String filePath = dto.getFilePath() + "/" + dto.getFileName();
Path path = Paths.get(filePath);
try {
boolean isDeleted = Files.deleteIfExists(path);
if (isDeleted) {
System.out.println("파일 삭제 성공: " + filePath);
} else {
System.out.println("삭제 실패: 파일이 존재하지 않습니다.");
}
} catch (IOException e) {
System.err.println("파일 삭제 중 오류 발생: " + e.getMessage());
}
DmlReturn dmlReturn = mapSheetMngCoreService.deleteByFileUidMngFile(uid);
return new DmlReturn("success", "파일체크");
}
// 중복제거 확인후 처리상태(DONE)변경
if (hstUid > 0) mapSheetMngCoreService.updateByHstUidSyncCheckState(hstUid);
public FoldersDto getFolderAll(SrchFoldersDto srchDto) {
return new DmlReturn("success", fileUids.size() + "개 파일이 삭제되었습니다.");
Path startPath = Paths.get(syncRootDir + srchDto.getDirPath());
String dirPath = syncRootDir + srchDto.getDirPath();
String sortType = "name desc";
List<FIleChecker.Folder> folderList = FIleChecker.getFolderAll(dirPath);
int folderTotCnt = folderList.size();
int folderErrTotCnt =
(int)
folderList.stream().filter(dto -> dto.getIsValid().toString().equals("false")).count();
return new FoldersDto(dirPath, folderTotCnt, folderErrTotCnt, folderList);
}
public FilesDto getFilesAll(SrchFilesDto srchDto) {
String dirPath = srchDto.getDirPath();
int startPos = srchDto.getStartPos();
int endPos = srchDto.getEndPos();
List<FIleChecker.Basic> files =
FIleChecker.getFilesFromAllDepth(
srchDto.getDirPath(),
"*",
srchDto.getExtension(),
1,
srchDto.getSortType(),
startPos,
endPos);
int fileListPos = 0;
int fileTotCnt = files.size();
long fileTotSize = FIleChecker.getFileTotSize(files);
return new FilesDto(dirPath, fileTotCnt, fileTotSize, files);
}
}

View File

@@ -93,6 +93,7 @@ public class ModelMngDto {
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModelList {
private Integer rowNum;

View File

@@ -6,7 +6,7 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheet;
import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity;
import com.kamco.cd.kamcoback.postgres.repository.Inference.InferenceResultRepository;
import com.kamco.cd.kamcoback.postgres.repository.Inference.MapSheetAnalDataRepository;
import com.kamco.cd.kamcoback.postgres.repository.scene.MapInkx5kRepository;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.constraints.NotNull;
@@ -20,7 +20,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class InferenceResultCoreService {
private final InferenceResultRepository inferenceResultRepository;
private final MapSheetAnalDataRepository mapSheetAnalDataRepository;
private final MapInkx5kRepository mapInkx5kRepository;
/**
@@ -31,7 +31,7 @@ public class InferenceResultCoreService {
*/
public Page<InferenceResultDto.AnalResList> getInferenceResultList(
InferenceResultDto.SearchReq searchReq) {
return inferenceResultRepository.getInferenceResultList(searchReq);
return mapSheetAnalDataRepository.getInferenceResultList(searchReq);
}
/**
@@ -42,7 +42,7 @@ public class InferenceResultCoreService {
*/
public InferenceResultDto.AnalResSummary getInferenceResultSummary(Long id) {
InferenceResultDto.AnalResSummary summary =
inferenceResultRepository
mapSheetAnalDataRepository
.getInferenceResultSummary(id)
.orElseThrow(() -> new EntityNotFoundException("요약정보를 찾을 수 없습니다. " + id));
return summary;
@@ -55,7 +55,7 @@ public class InferenceResultCoreService {
* @return
*/
public List<Dashboard> getDashboard(Long id) {
return inferenceResultRepository.getDashboard(id);
return mapSheetAnalDataRepository.getDashboard(id);
}
/**
@@ -66,7 +66,7 @@ public class InferenceResultCoreService {
*/
public Page<InferenceResultDto.Geom> getInferenceResultGeomList(
Long id, InferenceResultDto.SearchGeoReq searchGeoReq) {
return inferenceResultRepository.getInferenceGeomList(id, searchGeoReq);
return mapSheetAnalDataRepository.getInferenceGeomList(id, searchGeoReq);
}
/**
@@ -80,13 +80,13 @@ public class InferenceResultCoreService {
@NotNull Long analyId, InferenceResultDto.SearchGeoReq searchReq) {
// 분석 ID 에 해당하는 dataids를 가져온다.
List<Long> dataIds =
inferenceResultRepository.listAnalyGeom(analyId).stream()
mapSheetAnalDataRepository.listAnalyGeom(analyId).stream()
.mapToLong(MapSheetAnalDataEntity::getId)
.boxed()
.toList();
// 해당데이터의 폴리곤데이터를 가져온다
Page<MapSheetAnalDataGeomEntity> mapSheetAnalDataGeomEntities =
inferenceResultRepository.listInferenceResultWithGeom(dataIds, searchReq);
mapSheetAnalDataRepository.listInferenceResultWithGeom(dataIds, searchReq);
return mapSheetAnalDataGeomEntities.map(MapSheetAnalDataGeomEntity::toEntity);
}
@@ -97,13 +97,13 @@ public class InferenceResultCoreService {
* @return
*/
public List<Long> getSheets(Long id) {
return inferenceResultRepository.getSheets(id);
return mapSheetAnalDataRepository.getSheets(id);
}
@Transactional(readOnly = true)
public List<MapSheet> listGetScenes5k(Long analyId) {
List<String> sceneCodes =
inferenceResultRepository.listAnalyGeom(analyId).stream()
mapSheetAnalDataRepository.listAnalyGeom(analyId).stream()
.mapToLong(MapSheetAnalDataEntity::getMapSheetNum)
.mapToObj(String::valueOf)
.toList();

View File

@@ -0,0 +1,51 @@
package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import com.kamco.cd.kamcoback.postgres.repository.Inference.InferenceResultRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class InferenceResultShpCoreService {
private final InferenceResultRepository repo;
/** inference_results -> (inference, geom) upsert */
@Transactional
public void buildInferenceData() {
repo.upsertGroupsFromInferenceResults();
repo.upsertGeomsFromInferenceResults();
}
/** file_created_yn = false/null 인 data_uid 목록 */
@Transactional(readOnly = true)
public List<Long> findPendingDataUids(int limit) {
return repo.findPendingDataUids(limit);
}
/**
* 재생성 시작: 덮어쓰기 기준(무조건 처음부터) - inference.file_created_yn = false, file_created_dttm = null -
* geom(file_created_yn) 전부 false 리셋
*/
@Transactional
public void resetForRegenerate(Long dataUid) {
repo.resetInferenceCreated(dataUid);
repo.resetGeomCreatedByDataUid(dataUid);
}
/** 생성 대상 geom 엔티티 로드 (file_created_yn=false/null + geom not null) */
@Transactional(readOnly = true)
public List<MapSheetAnalDataInferenceGeomEntity> loadGeomEntities(Long dataUid, int limit) {
return repo.findGeomEntitiesByDataUid(dataUid, limit);
}
/** 성공 마킹: - 성공 geo_uid만 geom.file_created_yn=true - inference.file_created_yn=true */
@Transactional
public void markSuccess(Long dataUid, List<Long> geoUids) {
repo.markGeomCreatedByGeoUids(geoUids);
repo.markInferenceCreated(dataUid);
}
}

View File

@@ -6,16 +6,9 @@ import com.kamco.cd.kamcoback.postgres.entity.MapSheetMngHstEntity;
import com.kamco.cd.kamcoback.postgres.repository.mapsheet.MapSheetMngRepository;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.Valid;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
@@ -27,8 +20,6 @@ public class MapSheetMngCoreService {
private final MapSheetMngRepository mapSheetMngRepository;
private static final String ORIGINAL_IMAGES_PATH = "/app/original-images";
@Value("{spring.profiles.active}")
private String activeEnv;
@@ -52,10 +43,13 @@ public class MapSheetMngCoreService {
mapSheetMngRepository.deleteByHstUidMngFile(hstUid);
}
public int findByYearFileNameFileCount(int mngYyyy, String fileName) {
return mapSheetMngRepository.findByYearFileNameFileCount(mngYyyy, fileName);
}
public MapSheetMngDto.DmlReturn mngFileSave(@Valid MapSheetMngDto.MngFileAddReq addReq) {
mapSheetMngRepository.mngFileSave(addReq);
// int hstCnt = mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst(saved.getMngYyyy());
return new MapSheetMngDto.DmlReturn("success", "파일정보저장되었습니다.");
}
@@ -73,102 +67,16 @@ public class MapSheetMngCoreService {
return mapSheetMngRepository.findMapSheetError(hstUid);
}
public List<MapSheetMngDto.MngFilesDto> findIdToMapSheetFileList(Long hstUid) {
return mapSheetMngRepository.findIdToMapSheetFileList(hstUid);
public List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid) {
return mapSheetMngRepository.findByHstUidMapSheetFileList(hstUid);
}
public MapSheetMngDto.MngFilesDto findYyyyToMapSheetFilePathRefer(int mngYyyy) {
return mapSheetMngRepository.findYyyyToMapSheetFilePathRefer(mngYyyy);
}
public MapSheetMngDto.MngFilesDto findIdToMapSheetFile(Long fileUid) {
return mapSheetMngRepository.findIdToMapSheetFile(fileUid);
}
public MapSheetMngDto.DmlReturn uploadProcess(@Valid List<Long> hstUidList) {
int count = 0;
if (!Objects.isNull(hstUidList) && !hstUidList.isEmpty()) {
for (Long hstUid : hstUidList) {
Optional<MapSheetMngHstEntity> entity =
Optional.ofNullable(
mapSheetMngRepository
.findMapSheetMngHstInfo(hstUid)
.orElseThrow(EntityNotFoundException::new));
String localPath = "";
String rootDir = ORIGINAL_IMAGES_PATH + "/" + entity.get().getMngYyyy();
if (activeEnv.equals("local")) {
rootDir = localPath + rootDir;
}
String filename = entity.get().getMapSheetNum();
String[] extensions = {"tif", "tfw"};
boolean flag = allExtensionsExist(rootDir, filename, extensions);
if (flag) {
count += 1;
}
// 파일 크기 계산 및 저장
try (Stream<Path> paths = Files.walk(Paths.get(rootDir))) {
List<Path> matched =
paths
.filter(Files::isRegularFile)
.filter(
p -> {
String name = p.getFileName().toString();
return name.equals(filename + ".tif") || name.equals(filename + ".tfw");
})
.collect(Collectors.toList());
long tifSize =
matched.stream()
.filter(p -> p.getFileName().toString().endsWith(".tif"))
.mapToLong(
p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
long tfwSize =
matched.stream()
.filter(p -> p.getFileName().toString().endsWith(".tfw"))
.mapToLong(
p -> {
try {
return Files.size(p);
} catch (IOException e) {
return 0L;
}
})
.sum();
entity.get().setTifSizeBytes(tifSize);
entity.get().setTfwSizeBytes(tfwSize);
entity.get().setTotalSizeBytes(tifSize + tfwSize);
// 엔터티 저장 -> 커스텀 업데이트로 변경
mapSheetMngRepository.updateHstFileSizes(
entity.get().getHstUid(), tifSize, tfwSize, tifSize + tfwSize);
} catch (IOException e) {
// 크기 계산 실패 시 0으로 저장
entity.get().setTifSizeBytes(0L);
entity.get().setTfwSizeBytes(0L);
entity.get().setTotalSizeBytes(0L);
mapSheetMngRepository.updateHstFileSizes(entity.get().getHstUid(), 0L, 0L, 0L);
}
/*
MapSheetMngDto.DataState dataState =
flag ? MapSheetMngDto.DataState.SUCCESS : MapSheetMngDto.DataState.FAIL;
entity.get().updateDataState(dataState);
*/
}
}
return new MapSheetMngDto.DmlReturn("success", count + "개 업로드 성공하였습니다.");
public MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid) {
return mapSheetMngRepository.findByFileUidMapSheetFile(fileUid);
}
public MapSheetMngDto.DmlReturn updateExceptUseInference(@Valid List<Long> hstUidList) {
@@ -186,40 +94,7 @@ public class MapSheetMngCoreService {
return new MapSheetMngDto.DmlReturn("success", hstUidList.size() + "개 추론제외 업데이트 하였습니다.");
}
/**
* 특정 파일명 + 여러 확장자가 모두 존재하는지 확인
*
* @param rootDir 검색할 최상위 디렉토리
* @param filename 파일명 (확장자 제외)
* @param extensions 확인할 확장자 배열 (예: {"tif", "tfw"})
* @return 모든 확장자가 존재하면 true, 하나라도 없으면 false
*/
public static boolean allExtensionsExist(String rootDir, String filename, String... extensions) {
try (Stream<Path> paths = Files.walk(Paths.get(rootDir))) {
// 모든 파일명을 Set으로 저장
Set<String> fileNames =
paths
.filter(Files::isRegularFile)
.map(p -> p.getFileName().toString())
.collect(Collectors.toSet());
// 모든 확장자 파일 존재 여부 확인
for (String ext : extensions) {
String target = filename + "." + ext;
if (!fileNames.contains(target)) {
return false; // 하나라도 없으면 false
}
}
return true; // 모두 존재하면 true
} catch (IOException e) {
throw new RuntimeException("File search error", e);
}
}
public MapSheetMngDto.DmlReturn mngDataSave(@Valid MapSheetMngDto.AddReq addReq) {
public int mngDataSave(@Valid MapSheetMngDto.AddReq addReq) {
MapSheetMngEntity entity = new MapSheetMngEntity();
entity.setMngYyyy(addReq.getMngYyyy());
@@ -228,45 +103,31 @@ public class MapSheetMngCoreService {
mapSheetMngRepository.deleteByMngYyyyMngAll(addReq.getMngYyyy());
MapSheetMngEntity saved = mapSheetMngRepository.save(entity);
int hstCnt = mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst(saved.getMngYyyy());
int hstCnt =
mapSheetMngRepository.insertMapSheetOrgDataToMapSheetMngHst(
saved.getMngYyyy(), saved.getMngPath());
mapSheetMngRepository.updateYearState(saved.getMngYyyy(), "DONE");
return new MapSheetMngDto.DmlReturn("success", saved.getMngYyyy().toString());
return hstCnt;
}
public List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) {
return mapSheetMngRepository.findHstUidToMapSheetFileList(hstUid);
}
public MapSheetMngDto.DmlReturn deleteByFileUidMngFile(Long fileUid) {
public void deleteByFileUidMngFile(Long fileUid) {
mapSheetMngRepository.deleteByFileUidMngFile(fileUid);
return new MapSheetMngDto.DmlReturn("success", fileUid + " : 삭제되었습니다.");
}
public MapSheetMngDto.DmlReturn updateByHstUidSyncCheckState(Long hstUid) {
MapSheetMngDto.SyncCheckStateReqUpdateDto reqDto =
new MapSheetMngDto.SyncCheckStateReqUpdateDto();
reqDto.setHstUid(hstUid);
List<MapSheetMngDto.MngFilesDto> filesDto =
mapSheetMngRepository.findHstUidToMapSheetFileList(hstUid);
for (MapSheetMngDto.MngFilesDto dto : filesDto) {
if (dto.getFileExt().equals("tif")) reqDto.setSyncCheckTifFileName(dto.getFileName());
else if (dto.getFileExt().equals("tfw")) reqDto.setSyncCheckTfwFileName(dto.getFileName());
reqDto.setFilePath(dto.getFilePath());
}
String fileState = "DONE";
if (filesDto.size() > 2) fileState = "DONE";
reqDto.setSyncCheckState(fileState);
public void updateByHstUidSyncCheckState(MapSheetMngDto.SyncCheckStateReqUpdateDto reqDto) {
mapSheetMngRepository.updateMapSheetMngHstSyncCheckState(reqDto);
mapSheetMngRepository.updateByHstUidMngFileState(hstUid, fileState);
}
return new MapSheetMngDto.DmlReturn("success", hstUid + " : 상태변경되었습니다.");
public void updateByFileUidFileState(Long fileUid, String fileState) {
mapSheetMngRepository.updateByFileUidMngFileState(fileUid, fileState);
}
public void deleteByNotInFileUidMngFile(Long hstUid, List<Long> fileUids) {
mapSheetMngRepository.deleteByNotInFileUidMngFile(hstUid, fileUids);
}
}

View File

@@ -0,0 +1,79 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
import org.locationtech.jts.geom.Geometry;
@Getter
@Setter
@Entity
@Table(name = "inference_results")
public class InferenceResultEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "uid", nullable = false)
private Long id;
@NotNull
@ColumnDefault("uuid_generate_v4()")
@Column(name = "uuid", nullable = false)
private UUID uuid;
@Column(name = "stage")
private Integer stage;
@Column(name = "cd_prob")
private Float cdProb;
@Column(name = "input1")
private Integer input1;
@Column(name = "input2")
private Integer input2;
@Column(name = "map_id")
private Long mapId;
@Size(max = 20)
@Column(name = "before_class", length = 20)
private String beforeClass;
@Column(name = "before_probability")
private Float beforeProbability;
@Size(max = 20)
@Column(name = "after_class", length = 20)
private String afterClass;
@Column(name = "after_probability")
private Float afterProbability;
@ColumnDefault("st_area(geometry)")
@Column(name = "area")
private Float area;
@NotNull
@ColumnDefault("now()")
@Column(name = "created_dttm", nullable = false)
private ZonedDateTime createdDttm;
@NotNull
@ColumnDefault("now()")
@Column(name = "updated_dttm", nullable = false)
private ZonedDateTime updatedDttm;
@Column(name = "geometry", columnDefinition = "geometry not null")
private Geometry geometry;
}

View File

@@ -0,0 +1,167 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@Entity
@Table(name = "tb_map_sheet_anal_data_inference")
public class MapSheetAnalDataInferenceEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "data_uid", nullable = false)
private Long id;
@Size(max = 128)
@Column(name = "data_name", length = 128)
private String dataName;
@Size(max = 255)
@Column(name = "data_path")
private String dataPath;
@Size(max = 128)
@Column(name = "data_type", length = 128)
private String dataType;
@Size(max = 128)
@Column(name = "data_crs_type", length = 128)
private String dataCrsType;
@Size(max = 255)
@Column(name = "data_crs_type_name")
private String dataCrsTypeName;
@ColumnDefault("now()")
@Column(name = "created_dttm")
private ZonedDateTime createdDttm;
@Column(name = "created_uid")
private Long createdUid;
@ColumnDefault("now()")
@Column(name = "updated_dttm")
private ZonedDateTime updatedDttm;
@Column(name = "updated_uid")
private Long updatedUid;
@Column(name = "compare_yyyy")
private Integer compareYyyy;
@Column(name = "target_yyyy")
private Integer targetYyyy;
@Column(name = "data_json", length = Integer.MAX_VALUE)
private String dataJson;
@Size(max = 20)
@ColumnDefault("'0'")
@Column(name = "data_state", length = 20)
private String dataState;
@ColumnDefault("now()")
@Column(name = "data_state_dttm")
private ZonedDateTime dataStateDttm;
@Column(name = "anal_strt_dttm")
private ZonedDateTime analStrtDttm;
@Column(name = "anal_end_dttm")
private ZonedDateTime analEndDttm;
@ColumnDefault("0")
@Column(name = "anal_sec")
private Long analSec;
@Size(max = 20)
@Column(name = "anal_state", length = 20)
private String analState;
@Column(name = "anal_uid")
private Long analUid;
@Column(name = "map_sheet_num")
private Long mapSheetNum;
@ColumnDefault("0")
@Column(name = "detecting_cnt")
private Long detectingCnt;
@ColumnDefault("0")
@Column(name = "pnu")
private Long pnu;
@Size(max = 20)
@Column(name = "down_state", length = 20)
private String downState;
@Column(name = "down_state_dttm")
private ZonedDateTime downStateDttm;
@Size(max = 20)
@Column(name = "fit_state", length = 20)
private String fitState;
@Column(name = "fit_state_dttm")
private ZonedDateTime fitStateDttm;
@Column(name = "labeler_uid")
private Long labelerUid;
@Size(max = 20)
@ColumnDefault("NULL")
@Column(name = "label_state", length = 20)
private String labelState;
@Column(name = "label_state_dttm")
private ZonedDateTime labelStateDttm;
@Column(name = "tester_uid")
private Long testerUid;
@Size(max = 20)
@Column(name = "test_state", length = 20)
private String testState;
@Column(name = "test_state_dttm")
private ZonedDateTime testStateDttm;
@Column(name = "fit_state_cmmnt", length = Integer.MAX_VALUE)
private String fitStateCmmnt;
@Column(name = "ref_map_sheet_num")
private Long refMapSheetNum;
@Column(name = "stage")
private Integer stage;
@Column(name = "file_created_yn")
private Boolean fileCreatedYn;
@Size(max = 100)
@Column(name = "m1", length = 100)
private String m1;
@Size(max = 100)
@Column(name = "m2", length = 100)
private String m2;
@Size(max = 100)
@Column(name = "m3", length = 100)
private String m3;
@Column(name = "file_created_dttm")
private ZonedDateTime fileCreatedDttm;
}

View File

@@ -0,0 +1,149 @@
package com.kamco.cd.kamcoback.postgres.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
import org.locationtech.jts.geom.Geometry;
@Getter
@Setter
@Entity
@Table(name = "tb_map_sheet_anal_data_inference_geom")
public class MapSheetAnalDataInferenceGeomEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "geo_uid")
private Long geoUid;
@Column(name = "cd_prob")
private Float cdProb;
@Size(max = 40)
@Column(name = "class_before_cd", length = 40)
private String classBeforeCd;
@Column(name = "class_before_prob")
private Float classBeforeProb;
@Size(max = 40)
@Column(name = "class_after_cd", length = 40)
private String classAfterCd;
@Column(name = "class_after_prob")
private Float classAfterProb;
@Column(name = "map_sheet_num")
private Long mapSheetNum;
@Column(name = "compare_yyyy")
private Integer compareYyyy;
@Column(name = "target_yyyy")
private Integer targetYyyy;
@Column(name = "area")
private Float area;
@Size(max = 100)
@Column(name = "geo_type", length = 100)
private String geoType;
@Column(name = "data_uid")
private Long dataUid;
@ColumnDefault("now()")
@Column(name = "created_dttm")
private ZonedDateTime createdDttm;
@Column(name = "created_uid")
private Long createdUid;
@ColumnDefault("now()")
@Column(name = "updated_dttm")
private ZonedDateTime updatedDttm;
@Column(name = "updated_uid")
private Long updatedUid;
@ColumnDefault("0")
@Column(name = "geom_cnt")
private Long geomCnt;
@ColumnDefault("0")
@Column(name = "pnu")
private Long pnu;
@Size(max = 20)
@ColumnDefault("'0'")
@Column(name = "fit_state", length = 20)
private String fitState;
@ColumnDefault("now()")
@Column(name = "fit_state_dttm")
private ZonedDateTime fitStateDttm;
@Column(name = "labeler_uid")
private Long labelerUid;
@Size(max = 20)
@ColumnDefault("'0'")
@Column(name = "label_state", length = 20)
private String labelState;
@ColumnDefault("now()")
@Column(name = "label_state_dttm")
private ZonedDateTime labelStateDttm;
@Column(name = "tester_uid")
private Long testerUid;
@Size(max = 20)
@ColumnDefault("'0'")
@Column(name = "test_state", length = 20)
private String testState;
@ColumnDefault("now()")
@Column(name = "test_state_dttm")
private ZonedDateTime testStateDttm;
@Column(name = "fit_state_cmmnt", length = Integer.MAX_VALUE)
private String fitStateCmmnt;
@Column(name = "ref_map_sheet_num")
private Long refMapSheetNum;
@ColumnDefault("uuid_generate_v4()")
@Column(name = "uuid")
private UUID uuid;
@Column(name = "stage")
private Integer stage;
@Column(name = "map_5k_id")
private Long map5kId;
@Column(name = "file_created_yn")
private Boolean fileCreatedYn;
@Column(name = "geom", columnDefinition = "geometry")
private Geometry geom;
@Column(name = "geom_center", columnDefinition = "geometry")
private Geometry geomCenter;
@Column(name = "before_geom", columnDefinition = "geometry")
private Geometry beforeGeom;
@Column(name = "file_created_dttm")
private ZonedDateTime fileCreatedDttm;
}

View File

@@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;
@Getter
@Setter
@@ -54,4 +55,9 @@ public class MapSheetMngFileEntity {
@Size(max = 20)
@Column(name = "file_state", length = 20)
private String fileState;
@NotNull
@ColumnDefault("false")
@Column(name = "file_del", nullable = false)
private Boolean fileDel = false;
}

View File

@@ -1,7 +1,7 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface InferenceResultRepository
extends JpaRepository<MapSheetAnalEntity, Long>, InferenceResultRepositoryCustom {}
extends JpaRepository<com.kamco.cd.kamcoback.postgres.entity.InferenceResultEntity, Long>,
InferenceResultRepositoryCustom {}

View File

@@ -1,31 +1,23 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity;
import jakarta.validation.constraints.NotNull;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
public interface InferenceResultRepositoryCustom {
Page<InferenceResultDto.AnalResList> getInferenceResultList(
InferenceResultDto.SearchReq searchReq);
int upsertGroupsFromInferenceResults();
Optional<InferenceResultDto.AnalResSummary> getInferenceResultSummary(Long id);
int upsertGeomsFromInferenceResults();
Page<InferenceResultDto.Geom> getInferenceGeomList(
Long id, InferenceResultDto.SearchGeoReq searchGeoReq);
List<Long> findPendingDataUids(int limit);
Page<MapSheetAnalDataGeomEntity> listInferenceResultWithGeom(
List<Long> dataIds, SearchGeoReq searchReq);
int resetInferenceCreated(Long dataUid);
List<Long> getSheets(Long id);
int markInferenceCreated(Long dataUid);
List<Dashboard> getDashboard(Long id);
int resetGeomCreatedByDataUid(Long dataUid);
List<MapSheetAnalDataEntity> listAnalyGeom(@NotNull Long id);
int markGeomCreatedByGeoUids(List<Long> geoUids);
List<MapSheetAnalDataInferenceGeomEntity> findGeomEntitiesByDataUid(Long dataUid, int limit);
}

View File

@@ -1,27 +1,14 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq;
import com.kamco.cd.kamcoback.postgres.entity.*;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.ArrayList;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;
@Repository
@@ -29,335 +16,215 @@ import org.springframework.stereotype.Repository;
public class InferenceResultRepositoryImpl implements InferenceResultRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QModelMngBakEntity tmm = QModelMngBakEntity.modelMngBakEntity;
private final QModelVerEntity tmv = QModelVerEntity.modelVerEntity;
private final QMapSheetAnalEntity mapSheetAnalEntity = QMapSheetAnalEntity.mapSheetAnalEntity;
private final QMapSheetAnalDataEntity mapSheetAnalDataEntity =
QMapSheetAnalDataEntity.mapSheetAnalDataEntity;
private final QMapSheetAnalDataGeomEntity mapSheetAnalDataGeomEntity =
QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
private final QMapSheetAnalSttcEntity mapSheetAnalSttcEntity =
QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity;
/**
* 분석결과 목록 조회
*
* @param searchReq
* @return
*/
@PersistenceContext private final EntityManager em;
private final QMapSheetAnalDataInferenceEntity i =
QMapSheetAnalDataInferenceEntity.mapSheetAnalDataInferenceEntity;
private final QMapSheetAnalDataInferenceGeomEntity g =
QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity;
// ===============================
// Upsert (Native only)
// ===============================
@Override
public Page<InferenceResultDto.AnalResList> getInferenceResultList(
InferenceResultDto.SearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
// "0000" 전체조회
BooleanBuilder builder = new BooleanBuilder();
if (searchReq.getStatCode() != null && !"0000".equals(searchReq.getStatCode())) {
builder.and(mapSheetAnalEntity.analState.eq(searchReq.getStatCode()));
public int upsertGroupsFromInferenceResults() {
String sql =
"""
INSERT INTO tb_map_sheet_anal_data_inference (
stage,
compare_yyyy,
target_yyyy,
map_sheet_num,
created_dttm,
updated_dttm,
file_created_yn,
detecting_cnt
)
SELECT
r.stage,
r.input1 AS compare_yyyy,
r.input2 AS target_yyyy,
r.map_id AS map_sheet_num,
now() AS created_dttm,
now() AS updated_dttm,
false AS file_created_yn,
count(*) AS detecting_cnt
FROM inference_results r
GROUP BY r.stage, r.input1, r.input2, r.map_id
ON CONFLICT (stage, compare_yyyy, target_yyyy, map_sheet_num)
DO UPDATE SET
updated_dttm = now(),
detecting_cnt = EXCLUDED.detecting_cnt
""";
return em.createNativeQuery(sql).executeUpdate();
}
// 제목
if (searchReq.getTitle() != null) {
builder.and(mapSheetAnalEntity.analTitle.like("%" + searchReq.getTitle() + "%"));
}
List<InferenceResultDto.AnalResList> content =
queryFactory
.select(
Projections.constructor(
InferenceResultDto.AnalResList.class,
mapSheetAnalEntity.id,
mapSheetAnalEntity.analTitle,
mapSheetAnalEntity.analMapSheet,
mapSheetAnalEntity.detectingCnt,
mapSheetAnalEntity.analStrtDttm,
mapSheetAnalEntity.analEndDttm,
mapSheetAnalEntity.analSec,
mapSheetAnalEntity.analPredSec,
mapSheetAnalEntity.analState,
Expressions.stringTemplate(
"fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState),
mapSheetAnalEntity.gukyuinUsed))
.from(mapSheetAnalEntity)
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(mapSheetAnalEntity.id.desc())
.fetch();
long total =
queryFactory
.select(mapSheetAnalEntity.id)
.from(mapSheetAnalEntity)
.where(builder)
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
/**
* 분석결과 요약정보
*
* @param id
* @return
*/
@Override
public Optional<InferenceResultDto.AnalResSummary> getInferenceResultSummary(Long id) {
public int upsertGeomsFromInferenceResults() {
// 1. 최신 버전 UID를 가져오는 서브쿼리
JPQLQuery<Long> latestVerUidSub =
JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id));
// class_after_prob 컬럼 매핑 오타 주의(여기 제대로 넣음)
String sql =
"""
INSERT INTO tb_map_sheet_anal_data_inference_geom (
uuid, stage, cd_prob, compare_yyyy, target_yyyy, map_sheet_num,
class_before_cd, class_before_prob, class_after_cd, class_after_prob,
geom, area, data_uid, created_dttm, updated_dttm,
file_created_yn
)
SELECT
x.uuid, x.stage, x.cd_prob, x.compare_yyyy, x.target_yyyy, x.map_sheet_num,
x.class_before_cd, x.class_before_prob, x.class_after_cd, x.class_after_prob,
x.geom, x.area, x.data_uid, x.created_dttm, x.updated_dttm,
false AS file_created_yn
FROM (
SELECT DISTINCT ON (r.uuid)
r.uuid,
r.stage,
r.cd_prob,
r.input1 AS compare_yyyy,
r.input2 AS target_yyyy,
r.map_id AS map_sheet_num,
r.before_class AS class_before_cd,
r.before_probability AS class_before_prob,
r.after_class AS class_after_cd,
r.after_probability AS class_after_prob,
CASE
WHEN r.geometry IS NULL THEN NULL
WHEN left(r.geometry, 2) = '01'
THEN ST_SetSRID(ST_GeomFromWKB(decode(r.geometry, 'hex')), 5186)
ELSE ST_GeomFromText(r.geometry, 5186)
END AS geom,
r.area,
di.data_uid,
r.created_dttm,
r.updated_dttm
FROM inference_results r
JOIN tb_map_sheet_anal_data_inference di
ON di.stage = r.stage
AND di.compare_yyyy = r.input1
AND di.target_yyyy = r.input2
AND di.map_sheet_num = r.map_id
ORDER BY r.uuid, r.updated_dttm DESC NULLS LAST, r.uid DESC
) x
ON CONFLICT (uuid)
DO UPDATE SET
stage = EXCLUDED.stage,
cd_prob = EXCLUDED.cd_prob,
compare_yyyy = EXCLUDED.compare_yyyy,
target_yyyy = EXCLUDED.target_yyyy,
map_sheet_num = EXCLUDED.map_sheet_num,
class_before_cd = EXCLUDED.class_before_cd,
class_before_prob = EXCLUDED.class_before_prob,
class_after_cd = EXCLUDED.class_after_cd,
class_after_prob = EXCLUDED.class_after_prob,
geom = EXCLUDED.geom,
area = EXCLUDED.area,
data_uid = EXCLUDED.data_uid,
updated_dttm = now()
""";
Optional<InferenceResultDto.AnalResSummary> content =
Optional.ofNullable(
queryFactory
.select(
Projections.constructor(
InferenceResultDto.AnalResSummary.class,
mapSheetAnalEntity.id,
mapSheetAnalEntity.analTitle,
tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"),
mapSheetAnalEntity.targetYyyy,
mapSheetAnalEntity.compareYyyy,
mapSheetAnalEntity.analMapSheet,
mapSheetAnalEntity.analStrtDttm,
mapSheetAnalEntity.analEndDttm,
mapSheetAnalEntity.analSec,
mapSheetAnalEntity.analPredSec,
mapSheetAnalEntity.resultUrl,
mapSheetAnalEntity.detectingCnt,
mapSheetAnalEntity.accuracy,
mapSheetAnalEntity.analState,
Expressions.stringTemplate(
"fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState)))
.from(mapSheetAnalEntity)
.leftJoin(tmm)
.on(mapSheetAnalEntity.modelUid.eq(tmm.id))
.leftJoin(tmv)
.on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub)))
.where(mapSheetAnalEntity.id.eq(id))
.fetchOne());
return content;
return em.createNativeQuery(sql).executeUpdate();
}
/**
* 분석결과 상세 class name별 탐지 개수
*
* @param id
* @return
*/
// ===============================
// Jobs
// ===============================
@Override
public List<Dashboard> getDashboard(Long id) {
public List<Long> findPendingDataUids(int limit) {
return queryFactory
.select(
Projections.constructor(
Dashboard.class,
mapSheetAnalSttcEntity.id.classAfterCd,
mapSheetAnalSttcEntity.classAfterCnt.sum()))
.from(mapSheetAnalSttcEntity)
.where(mapSheetAnalSttcEntity.id.analUid.eq(id))
.groupBy(mapSheetAnalSttcEntity.id.classAfterCd)
.orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc())
.select(i.id) // data_uid
.from(i)
.where(i.fileCreatedYn.isFalse().or(i.fileCreatedYn.isNull()))
.orderBy(i.id.asc())
.limit(limit)
.fetch();
}
// ===============================
// Reset / Mark (전부 ZonedDateTime)
// ===============================
@Override
public List<MapSheetAnalDataEntity> listAnalyGeom(Long id) {
QMapSheetAnalDataEntity analy = QMapSheetAnalDataEntity.mapSheetAnalDataEntity;
return queryFactory.selectFrom(analy).where(analy.analUid.eq(id)).fetch();
public int resetInferenceCreated(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now();
return (int)
queryFactory
.update(i)
.set(i.fileCreatedYn, false)
.set(i.fileCreatedDttm, (ZonedDateTime) null)
.set(i.updatedDttm, now)
.where(i.id.eq(dataUid))
.execute();
}
/**
* 분석결과 상세 목록
*
* @param searchReq
* @return
*/
@Override
public Page<MapSheetAnalDataGeomEntity> listInferenceResultWithGeom(
List<Long> ids, SearchGeoReq searchReq) {
public int markInferenceCreated(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now();
// 분석 차수
QMapSheetAnalDataGeomEntity detectedEntity =
QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
Pageable pageable = searchReq.toPageable();
// 검색조건
JPAQuery<MapSheetAnalDataGeomEntity> query =
return (int)
queryFactory
.selectFrom(detectedEntity)
.update(i)
.set(i.fileCreatedYn, true)
.set(i.fileCreatedDttm, now)
.set(i.updatedDttm, now)
.where(i.id.eq(dataUid))
.execute();
}
@Override
public int resetGeomCreatedByDataUid(Long dataUid) {
ZonedDateTime now = ZonedDateTime.now();
return (int)
queryFactory
.update(g)
.set(g.fileCreatedYn, false)
.set(g.fileCreatedDttm, (ZonedDateTime) null)
.set(g.updatedDttm, now) // ✅ 엔티티/Q가 ZonedDateTime이면 정상
.where(g.dataUid.eq(dataUid))
.execute();
}
@Override
public int markGeomCreatedByGeoUids(List<Long> geoUids) {
if (geoUids == null || geoUids.isEmpty()) {
return 0;
}
ZonedDateTime now = ZonedDateTime.now();
return (int)
queryFactory
.update(g)
.set(g.fileCreatedYn, true)
.set(g.fileCreatedDttm, now)
.set(g.updatedDttm, now)
.where(g.geoUid.in(geoUids))
.execute();
}
// ===============================
// Export source (Entity only)
// ===============================
@Override
public List<MapSheetAnalDataInferenceGeomEntity> findGeomEntitiesByDataUid(
Long dataUid, int limit) {
return queryFactory
.selectFrom(g)
.where(
detectedEntity.dataUid.in(ids),
eqTargetClass(detectedEntity, searchReq.getTargetClass()),
eqCompareClass(detectedEntity, searchReq.getCompareClass()),
containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum()));
// count
long total = query.fetchCount();
// Pageable에서 정렬 가져오기, 없으면 기본 정렬(createdDttm desc) 사용
List<OrderSpecifier<?>> orders = getOrderSpecifiers(pageable.getSort());
if (orders.isEmpty()) {
orders.add(detectedEntity.createdDttm.desc());
}
List<MapSheetAnalDataGeomEntity> content =
query
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(orders.toArray(new OrderSpecifier[0]))
.fetch();
return new PageImpl<>(content, pageable, total);
}
/**
* 분석결과 상세 목록
*
* @param searchGeoReq
* @return
*/
@Override
public Page<InferenceResultDto.Geom> getInferenceGeomList(Long id, SearchGeoReq searchGeoReq) {
Pageable pageable = searchGeoReq.toPageable();
BooleanBuilder builder = new BooleanBuilder();
// 추론결과 id
builder.and(mapSheetAnalEntity.id.eq(id));
// 기준년도 분류
if (searchGeoReq.getTargetClass() != null && !searchGeoReq.getTargetClass().equals("")) {
builder.and(
mapSheetAnalDataGeomEntity
.classAfterCd
.toLowerCase()
.eq(searchGeoReq.getTargetClass().toLowerCase()));
}
// 비교년도 분류
if (searchGeoReq.getCompareClass() != null && !searchGeoReq.getCompareClass().equals("")) {
builder.and(
mapSheetAnalDataGeomEntity
.classBeforeCd
.toLowerCase()
.eq(searchGeoReq.getCompareClass().toLowerCase()));
}
// 분석도엽
if (searchGeoReq.getMapSheetNum() != null && !searchGeoReq.getMapSheetNum().isEmpty()) {
List<Long> mapSheetNum = searchGeoReq.getMapSheetNum();
builder.and(mapSheetAnalDataGeomEntity.mapSheetNum.in(mapSheetNum));
}
List<InferenceResultDto.Geom> content =
queryFactory
.select(
Projections.constructor(
InferenceResultDto.Geom.class,
mapSheetAnalDataGeomEntity.compareYyyy,
mapSheetAnalDataGeomEntity.targetYyyy,
mapSheetAnalDataGeomEntity.classBeforeCd,
mapSheetAnalDataGeomEntity.classBeforeProb,
mapSheetAnalDataGeomEntity.classAfterCd,
mapSheetAnalDataGeomEntity.classAfterProb,
mapSheetAnalDataGeomEntity.mapSheetNum,
mapSheetAnalDataGeomEntity.geom,
mapSheetAnalDataGeomEntity.geomCenter))
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.join(mapSheetAnalDataGeomEntity)
.on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id))
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total =
queryFactory
.select(mapSheetAnalDataGeomEntity.id)
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.join(mapSheetAnalDataGeomEntity)
.on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id))
.where(builder)
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
/**
* 추론된 5000:1 도엽 목록
*
* @param id
* @return
*/
@Override
public List<Long> getSheets(Long id) {
return queryFactory
.select(mapSheetAnalDataEntity.mapSheetNum)
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.where(mapSheetAnalEntity.id.eq(id))
.groupBy(mapSheetAnalDataEntity.mapSheetNum)
g.dataUid.eq(dataUid),
g.geom.isNotNull(),
g.fileCreatedYn.isFalse().or(g.fileCreatedYn.isNull()))
.orderBy(g.geoUid.asc())
.limit(limit)
.fetch();
}
/** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */
@SuppressWarnings({"unchecked", "rawtypes"})
private List<OrderSpecifier<?>> getOrderSpecifiers(Sort sort) {
List<OrderSpecifier<?>> orders = new ArrayList<>();
if (sort.isSorted()) {
QMapSheetAnalDataGeomEntity entity = QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
for (Sort.Order order : sort) {
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
String property = order.getProperty();
// 유효한 필드만 처리
switch (property) {
case "classBeforeCd" -> orders.add(new OrderSpecifier(direction, entity.classBeforeCd));
case "classBeforeProb" ->
orders.add(new OrderSpecifier(direction, entity.classBeforeProb));
case "classAfterCd" -> orders.add(new OrderSpecifier(direction, entity.classAfterCd));
case "classAfterProb" -> orders.add(new OrderSpecifier(direction, entity.classAfterProb));
case "mapSheetNum" -> orders.add(new OrderSpecifier(direction, entity.mapSheetNum));
case "compareYyyy" -> orders.add(new OrderSpecifier(direction, entity.compareYyyy));
case "targetYyyy" -> orders.add(new OrderSpecifier(direction, entity.targetYyyy));
case "area" -> orders.add(new OrderSpecifier(direction, entity.area));
case "createdDttm" -> orders.add(new OrderSpecifier(direction, entity.createdDttm));
case "updatedDttm" -> orders.add(new OrderSpecifier(direction, entity.updatedDttm));
// 유효하지 않은 필드는 무시
default -> {}
}
}
}
return orders;
}
private BooleanExpression eqTargetClass(
QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) {
return targetClass != null && !targetClass.isEmpty()
? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase())
: null;
}
private BooleanExpression eqCompareClass(
QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) {
return compareClass != null && !compareClass.isEmpty()
? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase())
: null;
}
private BooleanExpression containsMapSheetNum(
QMapSheetAnalDataGeomEntity detectedEntity, List<Long> mapSheet) {
if (mapSheet == null || mapSheet.isEmpty()) {
return null;
}
return detectedEntity.mapSheetNum.in(mapSheet);
}
}

View File

@@ -0,0 +1,8 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceGeomEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MapSheetAnalDataInferenceGeomRepository
extends JpaRepository<MapSheetAnalDataInferenceGeomEntity, Long>,
MapSheetAnalDataInferenceGeomRepositoryCustom {}

View File

@@ -0,0 +1,3 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
public interface MapSheetAnalDataInferenceGeomRepositoryCustom {}

View File

@@ -0,0 +1,9 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class MapSheetAnalDataInferenceGeomRepositoryImpl
implements MapSheetAnalDataInferenceGeomRepositoryCustom {}

View File

@@ -0,0 +1,8 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MapSheetAnalDataInferenceRepository
extends JpaRepository<MapSheetAnalDataInferenceEntity, Long>,
MapSheetAnalDataInferenceRepositoryCustom {}

View File

@@ -0,0 +1,3 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
public interface MapSheetAnalDataInferenceRepositoryCustom {}

View File

@@ -0,0 +1,13 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class MapSheetAnalDataInferenceRepositoryImpl
implements MapSheetAnalDataInferenceRepositoryCustom {
private final JPAQueryFactory queryFactory;
}

View File

@@ -0,0 +1,7 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MapSheetAnalDataRepository
extends JpaRepository<MapSheetAnalEntity, Long>, MapSheetAnalDataRepositoryCustom {}

View File

@@ -0,0 +1,32 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.AnalResList;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.AnalResSummary;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
public interface MapSheetAnalDataRepositoryCustom {
Page<AnalResList> getInferenceResultList(InferenceResultDto.SearchReq searchReq);
Optional<AnalResSummary> getInferenceResultSummary(Long id);
Page<InferenceResultDto.Geom> getInferenceGeomList(
Long id, InferenceResultDto.SearchGeoReq searchGeoReq);
Page<MapSheetAnalDataGeomEntity> listInferenceResultWithGeom(
List<Long> dataIds, SearchGeoReq searchReq);
List<Long> getSheets(Long id);
List<Dashboard> getDashboard(Long id);
List<MapSheetAnalDataEntity> listAnalyGeom(@NotNull Long id);
}

View File

@@ -0,0 +1,371 @@
package com.kamco.cd.kamcoback.postgres.repository.Inference;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.AnalResList;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.AnalResSummary;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Dashboard;
import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.SearchGeoReq;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataEntity;
import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataGeomEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataGeomEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalSttcEntity;
import com.kamco.cd.kamcoback.postgres.entity.QModelMngBakEntity;
import com.kamco.cd.kamcoback.postgres.entity.QModelVerEntity;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class MapSheetAnalDataRepositoryImpl implements MapSheetAnalDataRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QModelMngBakEntity tmm = QModelMngBakEntity.modelMngBakEntity;
private final QModelVerEntity tmv = QModelVerEntity.modelVerEntity;
private final QMapSheetAnalEntity mapSheetAnalEntity = QMapSheetAnalEntity.mapSheetAnalEntity;
private final QMapSheetAnalDataEntity mapSheetAnalDataEntity =
QMapSheetAnalDataEntity.mapSheetAnalDataEntity;
private final QMapSheetAnalDataGeomEntity mapSheetAnalDataGeomEntity =
QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
private final QMapSheetAnalSttcEntity mapSheetAnalSttcEntity =
QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity;
/**
* 분석결과 목록 조회
*
* @param searchReq
* @return
*/
@Override
public Page<AnalResList> getInferenceResultList(InferenceResultDto.SearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
// "0000" 전체조회
BooleanBuilder builder = new BooleanBuilder();
if (searchReq.getStatCode() != null && !"0000".equals(searchReq.getStatCode())) {
builder.and(mapSheetAnalEntity.analState.eq(searchReq.getStatCode()));
}
// 제목
if (searchReq.getTitle() != null) {
builder.and(mapSheetAnalEntity.analTitle.like("%" + searchReq.getTitle() + "%"));
}
List<AnalResList> content =
queryFactory
.select(
Projections.constructor(
InferenceResultDto.AnalResList.class,
mapSheetAnalEntity.id,
mapSheetAnalEntity.analTitle,
mapSheetAnalEntity.analMapSheet,
mapSheetAnalEntity.detectingCnt,
mapSheetAnalEntity.analStrtDttm,
mapSheetAnalEntity.analEndDttm,
mapSheetAnalEntity.analSec,
mapSheetAnalEntity.analPredSec,
mapSheetAnalEntity.analState,
Expressions.stringTemplate(
"fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState),
mapSheetAnalEntity.gukyuinUsed))
.from(mapSheetAnalEntity)
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(mapSheetAnalEntity.id.desc())
.fetch();
long total =
queryFactory
.select(mapSheetAnalEntity.id)
.from(mapSheetAnalEntity)
.where(builder)
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
/**
* 분석결과 요약정보
*
* @param id
* @return
*/
@Override
public Optional<AnalResSummary> getInferenceResultSummary(Long id) {
// 1. 최신 버전 UID를 가져오는 서브쿼리
JPQLQuery<Long> latestVerUidSub =
JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id));
Optional<InferenceResultDto.AnalResSummary> content =
Optional.ofNullable(
queryFactory
.select(
Projections.constructor(
InferenceResultDto.AnalResSummary.class,
mapSheetAnalEntity.id,
mapSheetAnalEntity.analTitle,
tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"),
mapSheetAnalEntity.targetYyyy,
mapSheetAnalEntity.compareYyyy,
mapSheetAnalEntity.analMapSheet,
mapSheetAnalEntity.analStrtDttm,
mapSheetAnalEntity.analEndDttm,
mapSheetAnalEntity.analSec,
mapSheetAnalEntity.analPredSec,
mapSheetAnalEntity.resultUrl,
mapSheetAnalEntity.detectingCnt,
mapSheetAnalEntity.accuracy,
mapSheetAnalEntity.analState,
Expressions.stringTemplate(
"fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState)))
.from(mapSheetAnalEntity)
.leftJoin(tmm)
.on(mapSheetAnalEntity.modelUid.eq(tmm.id))
.leftJoin(tmv)
.on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub)))
.where(mapSheetAnalEntity.id.eq(id))
.fetchOne());
return content;
}
/**
* 분석결과 상세 class name별 탐지 개수
*
* @param id
* @return
*/
@Override
public List<Dashboard> getDashboard(Long id) {
return queryFactory
.select(
Projections.constructor(
Dashboard.class,
mapSheetAnalSttcEntity.id.classAfterCd,
mapSheetAnalSttcEntity.classAfterCnt.sum()))
.from(mapSheetAnalSttcEntity)
.where(mapSheetAnalSttcEntity.id.analUid.eq(id))
.groupBy(mapSheetAnalSttcEntity.id.classAfterCd)
.orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc())
.fetch();
}
@Override
public List<MapSheetAnalDataEntity> listAnalyGeom(Long id) {
QMapSheetAnalDataEntity analy = QMapSheetAnalDataEntity.mapSheetAnalDataEntity;
return queryFactory.selectFrom(analy).where(analy.analUid.eq(id)).fetch();
}
/**
* 분석결과 상세 목록
*
* @param searchReq
* @return
*/
@Override
public Page<MapSheetAnalDataGeomEntity> listInferenceResultWithGeom(
List<Long> ids, SearchGeoReq searchReq) {
// 분석 차수
QMapSheetAnalDataGeomEntity detectedEntity =
QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
Pageable pageable = searchReq.toPageable();
// 검색조건
JPAQuery<MapSheetAnalDataGeomEntity> query =
queryFactory
.selectFrom(detectedEntity)
.where(
detectedEntity.dataUid.in(ids),
eqTargetClass(detectedEntity, searchReq.getTargetClass()),
eqCompareClass(detectedEntity, searchReq.getCompareClass()),
containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum()));
// count
long total = query.fetchCount();
// Pageable에서 정렬 가져오기, 없으면 기본 정렬(createdDttm desc) 사용
List<OrderSpecifier<?>> orders = getOrderSpecifiers(pageable.getSort());
if (orders.isEmpty()) {
orders.add(detectedEntity.createdDttm.desc());
}
List<MapSheetAnalDataGeomEntity> content =
query
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(orders.toArray(new OrderSpecifier[0]))
.fetch();
return new PageImpl<>(content, pageable, total);
}
/**
* 분석결과 상세 목록
*
* @param searchGeoReq
* @return
*/
@Override
public Page<InferenceResultDto.Geom> getInferenceGeomList(Long id, SearchGeoReq searchGeoReq) {
Pageable pageable = searchGeoReq.toPageable();
BooleanBuilder builder = new BooleanBuilder();
// 추론결과 id
builder.and(mapSheetAnalEntity.id.eq(id));
// 기준년도 분류
if (searchGeoReq.getTargetClass() != null && !searchGeoReq.getTargetClass().equals("")) {
builder.and(
mapSheetAnalDataGeomEntity
.classAfterCd
.toLowerCase()
.eq(searchGeoReq.getTargetClass().toLowerCase()));
}
// 비교년도 분류
if (searchGeoReq.getCompareClass() != null && !searchGeoReq.getCompareClass().equals("")) {
builder.and(
mapSheetAnalDataGeomEntity
.classBeforeCd
.toLowerCase()
.eq(searchGeoReq.getCompareClass().toLowerCase()));
}
// 분석도엽
if (searchGeoReq.getMapSheetNum() != null && !searchGeoReq.getMapSheetNum().isEmpty()) {
List<Long> mapSheetNum = searchGeoReq.getMapSheetNum();
builder.and(mapSheetAnalDataGeomEntity.mapSheetNum.in(mapSheetNum));
}
List<InferenceResultDto.Geom> content =
queryFactory
.select(
Projections.constructor(
InferenceResultDto.Geom.class,
mapSheetAnalDataGeomEntity.compareYyyy,
mapSheetAnalDataGeomEntity.targetYyyy,
mapSheetAnalDataGeomEntity.classBeforeCd,
mapSheetAnalDataGeomEntity.classBeforeProb,
mapSheetAnalDataGeomEntity.classAfterCd,
mapSheetAnalDataGeomEntity.classAfterProb,
mapSheetAnalDataGeomEntity.mapSheetNum,
mapSheetAnalDataGeomEntity.geom,
mapSheetAnalDataGeomEntity.geomCenter))
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.join(mapSheetAnalDataGeomEntity)
.on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id))
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total =
queryFactory
.select(mapSheetAnalDataGeomEntity.id)
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.join(mapSheetAnalDataGeomEntity)
.on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id))
.where(builder)
.fetchCount();
return new PageImpl<>(content, pageable, total);
}
/**
* 추론된 5000:1 도엽 목록
*
* @param id
* @return
*/
@Override
public List<Long> getSheets(Long id) {
return queryFactory
.select(mapSheetAnalDataEntity.mapSheetNum)
.from(mapSheetAnalEntity)
.join(mapSheetAnalDataEntity)
.on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id))
.where(mapSheetAnalEntity.id.eq(id))
.groupBy(mapSheetAnalDataEntity.mapSheetNum)
.fetch();
}
/** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */
@SuppressWarnings({"unchecked", "rawtypes"})
private List<OrderSpecifier<?>> getOrderSpecifiers(Sort sort) {
List<OrderSpecifier<?>> orders = new ArrayList<>();
if (sort.isSorted()) {
QMapSheetAnalDataGeomEntity entity = QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity;
for (Sort.Order order : sort) {
Order direction = order.isAscending() ? Order.ASC : Order.DESC;
String property = order.getProperty();
// 유효한 필드만 처리
switch (property) {
case "classBeforeCd" -> orders.add(new OrderSpecifier(direction, entity.classBeforeCd));
case "classBeforeProb" ->
orders.add(new OrderSpecifier(direction, entity.classBeforeProb));
case "classAfterCd" -> orders.add(new OrderSpecifier(direction, entity.classAfterCd));
case "classAfterProb" -> orders.add(new OrderSpecifier(direction, entity.classAfterProb));
case "mapSheetNum" -> orders.add(new OrderSpecifier(direction, entity.mapSheetNum));
case "compareYyyy" -> orders.add(new OrderSpecifier(direction, entity.compareYyyy));
case "targetYyyy" -> orders.add(new OrderSpecifier(direction, entity.targetYyyy));
case "area" -> orders.add(new OrderSpecifier(direction, entity.area));
case "createdDttm" -> orders.add(new OrderSpecifier(direction, entity.createdDttm));
case "updatedDttm" -> orders.add(new OrderSpecifier(direction, entity.updatedDttm));
// 유효하지 않은 필드는 무시
default -> {}
}
}
}
return orders;
}
private BooleanExpression eqTargetClass(
QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) {
return targetClass != null && !targetClass.isEmpty()
? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase())
: null;
}
private BooleanExpression eqCompareClass(
QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) {
return compareClass != null && !compareClass.isEmpty()
? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase())
: null;
}
private BooleanExpression containsMapSheetNum(
QMapSheetAnalDataGeomEntity detectedEntity, List<Long> mapSheet) {
if (mapSheet == null || mapSheet.isEmpty()) {
return null;
}
return detectedEntity.mapSheetNum.in(mapSheet);
}
}

View File

@@ -19,7 +19,7 @@ public interface MapSheetMngRepositoryCustom {
Optional<MapSheetMngHstEntity> findMapSheetMngHstInfo(Long hstUid);
int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy);
int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath);
List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid);
@@ -43,6 +43,10 @@ public interface MapSheetMngRepositoryCustom {
void updateByHstUidMngFileState(Long hstUid, String fileState);
void updateByFileUidMngFileState(Long fileUid, String fileState);
void deleteByNotInFileUidMngFile(Long hstUid, List<Long> fileUids);
void updateYearState(int yyyy, String status);
Page<MapSheetMngDto.ErrorDataDto> findMapSheetErrorList(
@@ -50,9 +54,11 @@ public interface MapSheetMngRepositoryCustom {
MapSheetMngDto.ErrorDataDto findMapSheetError(Long hstUid);
List<MapSheetMngDto.MngFilesDto> findIdToMapSheetFileList(Long hstUid);
List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid);
MapSheetMngDto.MngFilesDto findIdToMapSheetFile(Long fileUid);
MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid);
void updateHstFileSizes(Long hstUid, long tifSizeBytes, long tfwSizeBytes, long totalSizeBytes);
int findByYearFileNameFileCount(int mngYyyy, String fileName);
}

View File

@@ -448,10 +448,11 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
}
@Override
public List<MapSheetMngDto.MngFilesDto> findIdToMapSheetFileList(Long hstUid) {
public List<MapSheetMngDto.MngFilesDto> findByHstUidMapSheetFileList(Long hstUid) {
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngFileEntity.hstUid.eq(hstUid));
whereBuilder.and(mapSheetMngFileEntity.fileDel.eq(false));
List<MapSheetMngDto.MngFilesDto> foundContent =
queryFactory
@@ -478,6 +479,7 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
public List<MapSheetMngDto.MngFilesDto> findHstUidToMapSheetFileList(Long hstUid) {
BooleanBuilder whereBuilder = new BooleanBuilder();
whereBuilder.and(mapSheetMngFileEntity.hstUid.eq(hstUid));
whereBuilder.and(mapSheetMngFileEntity.fileDel.eq(false));
List<MapSheetMngDto.MngFilesDto> foundContent =
queryFactory
@@ -531,7 +533,7 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
}
@Override
public MapSheetMngDto.MngFilesDto findIdToMapSheetFile(Long fileUid) {
public MapSheetMngDto.MngFilesDto findByFileUidMapSheetFile(Long fileUid) {
MapSheetMngDto.MngFilesDto foundContent =
queryFactory
@@ -636,8 +638,9 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
public void deleteByHstUidMngFile(Long hstUid) {
long deletedFileCount =
queryFactory
.delete(mapSheetMngFileEntity)
.where(mapSheetMngFileEntity.fileUid.eq(hstUid))
.update(mapSheetMngFileEntity)
.set(mapSheetMngFileEntity.fileDel, true)
.where(mapSheetMngFileEntity.hstUid.eq(hstUid))
.execute();
}
@@ -645,7 +648,8 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
public void deleteByFileUidMngFile(Long fileUid) {
long fileCount =
queryFactory
.delete(mapSheetMngFileEntity)
.update(mapSheetMngFileEntity)
.set(mapSheetMngFileEntity.fileDel, true)
.where(mapSheetMngFileEntity.fileUid.eq(fileUid))
.execute();
}
@@ -660,6 +664,46 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
.execute();
}
@Override
public void deleteByNotInFileUidMngFile(Long hstUid, List<Long> fileUids) {
long execCount =
queryFactory
.update(mapSheetMngFileEntity)
.set(mapSheetMngFileEntity.fileDel, true)
.where(
mapSheetMngFileEntity
.hstUid
.eq(hstUid)
.and(mapSheetMngFileEntity.fileUid.notIn(fileUids)))
.execute();
}
@Override
public void updateByFileUidMngFileState(Long fileUid, String fileState) {
long execCount =
queryFactory
.update(mapSheetMngFileEntity)
.set(mapSheetMngFileEntity.fileState, fileState)
.where(mapSheetMngFileEntity.fileUid.eq(fileUid))
.execute();
}
@Override
public int findByYearFileNameFileCount(int mngYyyy, String fileName) {
Long execCount =
queryFactory
.select(mapSheetMngFileEntity.count())
.from(mapSheetMngFileEntity)
.where(
mapSheetMngFileEntity
.mngYyyy
.eq(mngYyyy)
.and(mapSheetMngFileEntity.fileName.eq(fileName)))
.fetchOne();
return Math.toIntExact(execCount);
}
@Override
public void mngFileSave(@Valid MapSheetMngDto.MngFileAddReq addReq) {
long fileCount =
@@ -689,13 +733,14 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
}
@Override
public int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy) {
public int insertMapSheetOrgDataToMapSheetMngHst(int mngYyyy, String mngPath) {
String sql =
"""
INSERT INTO tb_map_sheet_mng_hst
(
mng_yyyy
,map_sheet_path
,map_sheet_code
,map_sheet_num
,map_sheet_name
@@ -705,14 +750,15 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
,use_inference
)
select
:mngYyyy as mng_yyyy
,fid as map_sheet_code
,mapidcd_no::INTEGER as map_sheet_num
,mapid_nm as map_sheet_name
,fid as map_sheet_code_src
,5000 as scale_ratio
,((mapidcd_no::INTEGER)/1000) as ref_map_sheet_num
,use_inference
:mngYyyy as mng_yyyy,
:mngPath as map_sheet_path,
fid as map_sheet_code,
mapidcd_no as map_sheet_num,
mapid_nm as map_sheet_name,
fid as map_sheet_code_src,
5000 as scale_ratio,
((mapidcd_no::INTEGER)/1000) as ref_map_sheet_num,
use_inference
from
tb_map_inkx_5k
""";
@@ -720,6 +766,7 @@ public class MapSheetMngRepositoryImpl extends QuerydslRepositorySupport
// Native Query 생성 및 실행
Query query = (Query) em.createNativeQuery(sql);
query.setParameter("mngYyyy", mngYyyy);
query.setParameter("mngPath", mngPath);
int exeCnt = query.executeUpdate(); // 실행 (영향받은 행의 개수 반환)

View File

@@ -66,6 +66,7 @@ management:
file:
#sync-root-dir: D:/app/original-images/
sync-root-dir: /app/original-images/
sync-tmp-dir: ${file.sync-root-dir}/tmp
sync-file-extention: tfw,tif