diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataReviewJobCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataReviewJobCoreService.java index bc47eeff..b5dd96e3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataReviewJobCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/TrainingDataReviewJobCoreService.java @@ -1,6 +1,9 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.postgres.repository.scheduler.TrainingDataReviewJobRepository; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalCntInfo; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalMapSheetList; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks; import java.util.List; @@ -41,4 +44,21 @@ public class TrainingDataReviewJobCoreService { public void updateGeomUidTestState(List geomUids) { trainingDataReviewJobRepository.updateGeomUidTestState(geomUids); } + + public List findCompletedYesterdayLabelingList( + Long analUid, String mapSheetNum) { + return trainingDataReviewJobRepository.findCompletedYesterdayLabelingList(analUid, mapSheetNum); + } + + public List findCompletedAnalMapSheetList(Long analUid) { + return trainingDataReviewJobRepository.findCompletedAnalMapSheetList(analUid); + } + + public List findAnalCntInfoList() { + return trainingDataReviewJobRepository.findAnalCntInfoList(); + } + + public void updateLearnDataGeomFileCreateYn(List geoUids) { + trainingDataReviewJobRepository.updateLearnDataGeomFileCreateYn(geoUids); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java index e5815451..798153d4 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java @@ -40,4 +40,7 @@ public class MapSheetLearnDataGeomEntity extends CommonDateEntity { @Column(name = "geom") private Geometry geom; + + @Column(name = "file_create_yn") + private Boolean fileCreateYn; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryCustom.java index f3030013..f4e13543 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryCustom.java @@ -1,5 +1,8 @@ package com.kamco.cd.kamcoback.postgres.repository.scheduler; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalCntInfo; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalMapSheetList; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks; import java.util.List; @@ -18,4 +21,12 @@ public interface TrainingDataReviewJobRepositoryCustom { Tasks findAssignmentTask(String assignmentUid); void updateGeomUidTestState(List geomUids); + + List findCompletedYesterdayLabelingList(Long analUid, String mapSheetNum); + + List findCompletedAnalMapSheetList(Long analUid); + + List findAnalCntInfoList(); + + void updateLearnDataGeomFileCreateYn(List geoUids); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryImpl.java index a7316cee..4dc89589 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scheduler/TrainingDataReviewJobRepositoryImpl.java @@ -3,17 +3,29 @@ package com.kamco.cd.kamcoback.postgres.repository.scheduler; import static com.kamco.cd.kamcoback.postgres.entity.QLabelingAssignmentEntity.labelingAssignmentEntity; import static com.kamco.cd.kamcoback.postgres.entity.QLabelingInspectorEntity.labelingInspectorEntity; import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalDataInferenceGeomEntity.mapSheetAnalDataInferenceGeomEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetAnalInferenceEntity.mapSheetAnalInferenceEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnDataGeomEntity.mapSheetLearnDataGeomEntity; +import static com.kamco.cd.kamcoback.postgres.entity.QMapSheetLearnEntity.mapSheetLearnEntity; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InspectState; +import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelMngState; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.LabelState; import com.kamco.cd.kamcoback.postgres.entity.LabelingAssignmentEntity; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalCntInfo; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalMapSheetList; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData.Properties; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.core.types.dsl.StringExpression; import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.transaction.Transactional; +import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.List; @@ -145,4 +157,132 @@ public class TrainingDataReviewJobRepositoryImpl extends QuerydslRepositorySuppo .where(mapSheetAnalDataInferenceGeomEntity.geoUid.in(geomUids)) .execute(); } + + @Override + public List findCompletedYesterdayLabelingList( + Long analUid, String mapSheetNum) { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + + // 오늘 날짜 (시간 없음) + LocalDate today = LocalDate.now(zoneId); + ZonedDateTime end = today.atStartOfDay(zoneId); // 오늘 00:00 + + return queryFactory + .select( + Projections.constructor( + CompleteLabelData.class, + mapSheetLearnDataGeomEntity.geoUid, + Expressions.stringTemplate("{0}", "Feature").as("type"), + Expressions.stringTemplate("ST_AsGeoJSON({0})", mapSheetLearnDataGeomEntity.geom), + Projections.constructor( + Properties.class, + new CaseBuilder() + .when(mapSheetLearnDataGeomEntity.classAfterCd.in("building", "container")) + .then("M1") + .when(mapSheetLearnDataGeomEntity.classAfterCd.eq("waste")) + .then("M2") + .otherwise("M3"), + mapSheetLearnDataGeomEntity.classBeforeCd, + mapSheetLearnDataGeomEntity.classAfterCd))) + .from(labelingAssignmentEntity) + .leftJoin(mapSheetLearnDataGeomEntity) + .on(labelingAssignmentEntity.inferenceGeomUid.eq(mapSheetLearnDataGeomEntity.geoUid)) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.assignGroupId.eq(mapSheetNum), + labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId()), + labelingAssignmentEntity.inspectStatDttm.lt(end)) + .fetch(); + } + + @Override + public List findCompletedAnalMapSheetList(Long analUid) { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + LocalDate today = LocalDate.now(zoneId); + ZonedDateTime end = today.atStartOfDay(zoneId); // 오늘 00:00 + + return queryFactory + .select( + Projections.constructor( + AnalMapSheetList.class, + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + labelingAssignmentEntity.assignGroupId)) + .from(labelingAssignmentEntity) + .innerJoin(mapSheetAnalInferenceEntity) + .on(labelingAssignmentEntity.analUid.eq(mapSheetAnalInferenceEntity.id)) + .where( + labelingAssignmentEntity.analUid.eq(analUid), + labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId()), + labelingAssignmentEntity.inspectStatDttm.lt(end)) + .groupBy( + mapSheetAnalInferenceEntity.compareYyyy, + mapSheetAnalInferenceEntity.targetYyyy, + labelingAssignmentEntity.assignGroupId) + .fetch(); + } + + @Override + public List findAnalCntInfoList() { + // 검수 제외(EXCEPT)를 뺀 나머지 cnt + NumberExpression allCnt = + new CaseBuilder() + .when( + labelingAssignmentEntity + .inspectState + .eq(InspectState.UNCONFIRM.getId()) + .or(labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId())) + .or(labelingAssignmentEntity.inspectState.isNull())) + .then(1L) + .otherwise(0L) + .sum(); + + // file_cnt + NumberExpression fileCnt = + new CaseBuilder() + .when(mapSheetLearnDataGeomEntity.fileCreateYn.isTrue()) + .then(1L) + .otherwise(0L) + .sum(); + + NumberExpression completeCnt = + new CaseBuilder() + .when(labelingAssignmentEntity.inspectState.eq(InspectState.COMPLETE.getId())) + .then(1L) + .otherwise(0L) + .sum(); + + return queryFactory + .select( + Projections.constructor( + AnalCntInfo.class, + labelingAssignmentEntity.analUid, + mapSheetLearnEntity.uid, + allCnt, + completeCnt, + fileCnt)) + .from(labelingAssignmentEntity) + .innerJoin(mapSheetAnalInferenceEntity) + .on( + labelingAssignmentEntity.analUid.eq(mapSheetAnalInferenceEntity.id), + mapSheetAnalInferenceEntity.analState.eq(LabelMngState.ING.getId())) + .leftJoin(mapSheetLearnEntity) + .on(mapSheetAnalInferenceEntity.learnId.eq(mapSheetLearnEntity.id)) + .leftJoin(mapSheetLearnDataGeomEntity) + .on(labelingAssignmentEntity.inferenceGeomUid.eq(mapSheetLearnDataGeomEntity.geoUid)) + .groupBy(labelingAssignmentEntity.analUid, mapSheetLearnEntity.uid) + .having(completeCnt.gt(0L)) + .fetch(); + } + + @Override + @Transactional + public void updateLearnDataGeomFileCreateYn(List geoUids) { + queryFactory + .update(mapSheetLearnDataGeomEntity) + .set(mapSheetLearnDataGeomEntity.fileCreateYn, true) + .set(mapSheetLearnDataGeomEntity.modifiedDate, ZonedDateTime.now()) + .where(mapSheetLearnDataGeomEntity.geoUid.in(geoUids)) + .execute(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/scheduler/dto/TrainingDataReviewJobDto.java b/src/main/java/com/kamco/cd/kamcoback/scheduler/dto/TrainingDataReviewJobDto.java index 343d9d4d..7d452c8d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scheduler/dto/TrainingDataReviewJobDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/scheduler/dto/TrainingDataReviewJobDto.java @@ -1,5 +1,13 @@ package com.kamco.cd.kamcoback.scheduler.dto; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData.GeoJsonFeature; +import java.util.List; import java.util.UUID; import lombok.AllArgsConstructor; import lombok.Getter; @@ -28,4 +36,102 @@ public class TrainingDataReviewJobDto { String inspectorUid; Long pendingCount; } + + @Getter + @Setter + @RequiredArgsConstructor + @AllArgsConstructor + public static class AnalCntInfo { + + Long analUid; + String resultUid; + Long allCnt; + Long completeCnt; + Long fileCnt; + } + + @Getter + @Setter + @RequiredArgsConstructor + @AllArgsConstructor + public static class AnalMapSheetList { + + private Integer compareYyyy; + private Integer targetYyyy; + private String mapSheetNum; + } + + @Getter + @Setter + @JsonPropertyOrder({"type", "features"}) + public static class FeatureCollection { + + private final String type = "FeatureCollection"; + private List features; + + public FeatureCollection(List features) { + this.features = features; + } + } + + @Getter + @Setter + @JsonPropertyOrder({"type", "geometry", "properties"}) + public static class CompleteLabelData { + + private Long geoUid; + private String type; + @JsonIgnore private String geomStr; + private JsonNode geometry; + private Properties properties; + + public CompleteLabelData(Long geoUid, String type, String geomStr, Properties properties) { + this.geoUid = geoUid; + this.type = type; + this.geomStr = geomStr; + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = null; + try { + if (geomStr != null) { + jsonNode = mapper.readTree(this.geomStr); + } + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + this.geometry = jsonNode; + if (jsonNode != null && jsonNode.isObject()) { + ((ObjectNode) jsonNode).remove("crs"); + } + + this.properties = properties; + } + + @Getter + @Setter + @RequiredArgsConstructor + @AllArgsConstructor + public static class Properties { + + private String modelId; + private String before; + private String after; + } + + @Getter + @AllArgsConstructor + public static class GeoJsonFeature { + + private String type; + private JsonNode geometry; + private Properties properties; + + public static GeoJsonFeature from(CompleteLabelData data) { + return new GeoJsonFeature( + data.getType(), + data.getGeometry(), // geoUid 없음 + data.getProperties()); + } + } + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/scheduler/service/TrainingDataReviewJobService.java b/src/main/java/com/kamco/cd/kamcoback/scheduler/service/TrainingDataReviewJobService.java index 6618278d..c69947e9 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scheduler/service/TrainingDataReviewJobService.java +++ b/src/main/java/com/kamco/cd/kamcoback/scheduler/service/TrainingDataReviewJobService.java @@ -1,13 +1,25 @@ package com.kamco.cd.kamcoback.scheduler.service; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.kamco.cd.kamcoback.postgres.core.TrainingDataReviewJobCoreService; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalCntInfo; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.AnalMapSheetList; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.CompleteLabelData.GeoJsonFeature; +import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.FeatureCollection; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.InspectorPendingDto; import com.kamco.cd.kamcoback.scheduler.dto.TrainingDataReviewJobDto.Tasks; import jakarta.transaction.Transactional; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -26,6 +38,9 @@ public class TrainingDataReviewJobService { @Value("${spring.profiles.active}") private String profile; + @Value("${training-data.geojson-dir}") + private String trainingDataDir; + private boolean isLocalProfile() { return "local".equalsIgnoreCase(profile); } @@ -134,4 +149,70 @@ public class TrainingDataReviewJobService { geomUids.add(task.getInferenceUid()); trainingDataReviewJobCoreService.updateGeomUidTestState(geomUids); } + + @Transactional + @Scheduled(cron = "0 0 2 * * *") + public void exportGeojsonLabelingGeom() { + + // 1) 경로/파일명 결정 + String targetDir = + "local".equals(profile) ? System.getProperty("user.home") + "/geojson" : trainingDataDir; + + // 2) 진행중인 회차 중, complete_cnt 가 존재하는 회차 목록 가져오기 + List analList = trainingDataReviewJobCoreService.findAnalCntInfoList(); + + for (AnalCntInfo info : analList) { + if (Objects.equals(info.getAllCnt(), info.getFileCnt())) { + continue; + } + + String resultUid = info.getResultUid(); // 회차의 대문자 uid (폴더명으로 사용) + + // 3) 회차 + 어제까지 검수 완료된 총 데이터의 도엽별 목록 가져오기 + List analMapList = + trainingDataReviewJobCoreService.findCompletedAnalMapSheetList(info.getAnalUid()); + + if (analMapList.isEmpty()) { + continue; + } + + for (AnalMapSheetList mapSheet : analMapList) { + // 4) 도엽별 geom 데이터 가지고 와서 geojson 만들기 + List completeList = + trainingDataReviewJobCoreService.findCompletedYesterdayLabelingList( + info.getAnalUid(), mapSheet.getMapSheetNum()); + + if (!completeList.isEmpty()) { + + List geoUids = completeList.stream().map(CompleteLabelData::getGeoUid).toList(); + List features = completeList.stream().map(GeoJsonFeature::from).toList(); + + // 5) 파일서버에 uid 폴더 생성 후 업로드 하기 + FeatureCollection collection = new FeatureCollection(features); + String filename = + String.format( + "%s_%s_%s_%s_D15.geojson", + resultUid.substring(0, 8), + mapSheet.getCompareYyyy(), + mapSheet.getTargetYyyy(), + mapSheet.getMapSheetNum()); + + Path outputPath = Paths.get(targetDir + "/" + resultUid, filename); + try { + Files.createDirectories(outputPath.getParent()); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + objectMapper.writeValue(outputPath.toFile(), collection); + + // geoUids : file_create_yn = true 로 업데이트 + trainingDataReviewJobCoreService.updateLearnDataGeomFileCreateYn(geoUids); + + } catch (IOException e) { + log.error(e.getMessage()); + } + } + } + } + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataReviewApiController.java b/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataReviewApiController.java index bf9318b5..c0b09ab2 100644 --- a/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataReviewApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/trainingdata/TrainingDataReviewApiController.java @@ -565,4 +565,11 @@ public class TrainingDataReviewApiController { trainingDataReviewJobService.assignReviewerYesterdayLabelComplete(); return ApiResponseDto.ok(null); } + + @Operation(summary = "검수완료된 라벨링 geojson 생성(스케줄링 수동 호출)", description = "검수완료된 라벨링 geojson 생성") + @GetMapping("/run-label-geojson") + public ApiResponseDto runExportGeojsonLabelingGeom() { + trainingDataReviewJobService.exportGeojsonLabelingGeom(); + return ApiResponseDto.ok(0L); + } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 41885480..e5e34fe3 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -115,3 +115,6 @@ gukyuin: #url: http://localhost:8080 url: http://192.168.2.129:5301 mast: ${gukyuin.url}/api/kcd/cdi/chn/mast + +training-data: + geojson-dir: /kamco-nfs/model_output/labeling/ diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 80542fde..46cd9a73 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -70,3 +70,6 @@ gukyuin: #url: http://localhost:8080 url: http://192.168.2.129:5301 mast: ${gukyuin.url}/api/kcd/cdi/chn/mast + +training-data: + geojson-dir: /kamco-nfs/model_output/labeling/